diff --git a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs index f4bbff2e..490dda6b 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs @@ -61,6 +61,8 @@ private void InitDefaultValues() _enableMultipleHttp2Connections = GrpcDefaultSettings.EnableMultipleHttp2Connections; _maxSendMessageSize = GrpcDefaultSettings.MaxSendMessageSize; _maxReceiveMessageSize = GrpcDefaultSettings.MaxReceiveMessageSize; + _pooledConnectionIdleTimeout = GrpcDefaultSettings.PooledConnectionIdleTimeoutSeconds; + _pooledConnectionLifetime = GrpcDefaultSettings.PooledConnectionLifetimeSeconds; _disableDiscovery = GrpcDefaultSettings.DisableDiscovery; _disableServerBalancer = false; _enableImplicitSession = false; @@ -425,6 +427,46 @@ public int MaxReceiveMessageSize private int _maxReceiveMessageSize; + /// + /// Gets or sets the idle timeout for pooled HTTP/2 connections in seconds. + /// + /// + /// Specifies how long a pooled connection can remain idle before being closed. + /// This helps prevent HTTP/2 protocol errors from long-lived connections. + /// Default value: 60 seconds. + /// + public int PooledConnectionIdleTimeout + { + get => _pooledConnectionIdleTimeout; + set + { + _pooledConnectionIdleTimeout = value; + SaveValue(nameof(PooledConnectionIdleTimeout), value); + } + } + + private int _pooledConnectionIdleTimeout; + + /// + /// Gets or sets the lifetime for pooled HTTP/2 connections in seconds. + /// + /// + /// Specifies the maximum lifetime for a pooled connection before being closed. + /// This helps prevent HTTP/2 protocol errors from long-lived connections. + /// Default value: 300 seconds (5 minutes). + /// + public int PooledConnectionLifetime + { + get => _pooledConnectionLifetime; + set + { + _pooledConnectionLifetime = value; + SaveValue(nameof(PooledConnectionLifetime), value); + } + } + + private int _pooledConnectionLifetime; + /// /// Gets or sets a value indicating whether to disable server load balancing. /// @@ -585,7 +627,8 @@ public override object this[string keyword] $"UseTls={UseTls};Host={Host};Port={Port};Database={Database};User={User};Password={Password};" + $"ConnectTimeout={ConnectTimeout};KeepAlivePingDelay={KeepAlivePingDelay};KeepAlivePingTimeout={KeepAlivePingTimeout};" + $"EnableMultipleHttp2Connections={EnableMultipleHttp2Connections};MaxSendMessageSize={MaxSendMessageSize};" + - $"MaxReceiveMessageSize={MaxReceiveMessageSize};DisableDiscovery={DisableDiscovery}"; + $"MaxReceiveMessageSize={MaxReceiveMessageSize};PooledConnectionIdleTimeout={PooledConnectionIdleTimeout};" + + $"PooledConnectionLifetime={PooledConnectionLifetime};DisableDiscovery={DisableDiscovery}"; internal async Task BuildDriver() { @@ -611,7 +654,13 @@ internal async Task BuildDriver() Password = Password, EnableMultipleHttp2Connections = EnableMultipleHttp2Connections, MaxSendMessageSize = MaxSendMessageSize, - MaxReceiveMessageSize = MaxReceiveMessageSize + MaxReceiveMessageSize = MaxReceiveMessageSize, + PooledConnectionIdleTimeout = PooledConnectionIdleTimeout == 0 + ? Timeout.InfiniteTimeSpan + : TimeSpan.FromSeconds(PooledConnectionIdleTimeout), + PooledConnectionLifetime = PooledConnectionLifetime == 0 + ? Timeout.InfiniteTimeSpan + : TimeSpan.FromSeconds(PooledConnectionLifetime) }; return DisableDiscovery @@ -707,6 +756,12 @@ static YdbConnectionOption() AddOption(new YdbConnectionOption(IntExtractor, (builder, maxReceiveMessageSize) => builder.MaxReceiveMessageSize = maxReceiveMessageSize), "MaxReceiveMessageSize", "Max Receive Message Size"); + AddOption(new YdbConnectionOption(IntExtractor, (builder, pooledConnectionIdleTimeout) => + builder.PooledConnectionIdleTimeout = pooledConnectionIdleTimeout), + "PooledConnectionIdleTimeout", "Pooled Connection Idle Timeout"); + AddOption(new YdbConnectionOption(IntExtractor, (builder, pooledConnectionLifetime) => + builder.PooledConnectionLifetime = pooledConnectionLifetime), + "PooledConnectionLifetime", "Pooled Connection Lifetime"); AddOption(new YdbConnectionOption(BoolExtractor, (builder, disableDiscovery) => builder.DisableDiscovery = disableDiscovery), "DisableDiscovery", "Disable Discovery"); AddOption(new YdbConnectionOption(IntExtractor, diff --git a/src/Ydb.Sdk/src/DriverConfig.cs b/src/Ydb.Sdk/src/DriverConfig.cs index 104adb02..41d8560c 100644 --- a/src/Ydb.Sdk/src/DriverConfig.cs +++ b/src/Ydb.Sdk/src/DriverConfig.cs @@ -73,6 +73,20 @@ public class DriverConfig /// public int MaxReceiveMessageSize { get; init; } = GrpcDefaultSettings.MaxReceiveMessageSize; + /// + /// Gets or sets the idle timeout for pooled HTTP/2 connections. + /// This helps prevent connection corruption from long-lived connections. + /// + public TimeSpan PooledConnectionIdleTimeout { get; init; } = + TimeSpan.FromSeconds(GrpcDefaultSettings.PooledConnectionIdleTimeoutSeconds); + + /// + /// Gets or sets the lifetime for pooled HTTP/2 connections. + /// This helps prevent connection corruption from long-lived connections. + /// + public TimeSpan PooledConnectionLifetime { get; init; } = + TimeSpan.FromSeconds(GrpcDefaultSettings.PooledConnectionLifetimeSeconds); + internal X509Certificate2Collection CustomServerCertificates { get; } = new(); internal TimeSpan EndpointDiscoveryInterval = TimeSpan.FromMinutes(1); internal TimeSpan EndpointDiscoveryTimeout = TimeSpan.FromSeconds(10); diff --git a/src/Ydb.Sdk/src/GrpcDefaultSettings.cs b/src/Ydb.Sdk/src/GrpcDefaultSettings.cs index 7f837e75..040981ef 100644 --- a/src/Ydb.Sdk/src/GrpcDefaultSettings.cs +++ b/src/Ydb.Sdk/src/GrpcDefaultSettings.cs @@ -21,4 +21,16 @@ internal static class GrpcDefaultSettings internal const bool EnableMultipleHttp2Connections = false; internal const bool DisableDiscovery = false; + + /// + /// Default idle timeout (in seconds) for pooled HTTP/2 connections. + /// Set to prevent connection corruption from long-lived connections. + /// + internal const int PooledConnectionIdleTimeoutSeconds = 60; + + /// + /// Default lifetime (in seconds) for pooled HTTP/2 connections. + /// Set to prevent connection corruption from long-lived connections. + /// + internal const int PooledConnectionLifetimeSeconds = 300; // 5 minutes } diff --git a/src/Ydb.Sdk/src/Pool/ChannelPool.cs b/src/Ydb.Sdk/src/Pool/ChannelPool.cs index 84a3c50d..7145177f 100644 --- a/src/Ydb.Sdk/src/Pool/ChannelPool.cs +++ b/src/Ydb.Sdk/src/Pool/ChannelPool.cs @@ -107,7 +107,11 @@ public GrpcChannel CreateChannel(string endpoint) KeepAlivePingDelay = _config.KeepAlivePingDelay, KeepAlivePingTimeout = _config.KeepAlivePingTimeout, KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always, - EnableMultipleHttp2Connections = _config.EnableMultipleHttp2Connections + EnableMultipleHttp2Connections = _config.EnableMultipleHttp2Connections, + // https://github.com/grpc/grpc-dotnet/issues/2641 + // Set connection pool timeouts to prevent HTTP/2 PROTOCOL_ERROR from long-lived connections + PooledConnectionIdleTimeout = _config.PooledConnectionIdleTimeout, + PooledConnectionLifetime = _config.PooledConnectionLifetime }; // https://github.com/grpc/grpc-dotnet/issues/2312#issuecomment-1790661801 diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs index a7915c5a..4bf71121 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs @@ -27,6 +27,8 @@ public void InitDefaultValues_WhenEmptyConstructorInvoke_ReturnDefaultConnection Assert.False(ydbConnectionStringBuilder.EnableMultipleHttp2Connections); Assert.Equal(MessageSize, ydbConnectionStringBuilder.MaxSendMessageSize); Assert.Equal(MessageSize, ydbConnectionStringBuilder.MaxReceiveMessageSize); + Assert.Equal(60, ydbConnectionStringBuilder.PooledConnectionIdleTimeout); + Assert.Equal(300, ydbConnectionStringBuilder.PooledConnectionLifetime); Assert.False(ydbConnectionStringBuilder.DisableDiscovery); Assert.False(ydbConnectionStringBuilder.DisableServerBalancer); Assert.False(ydbConnectionStringBuilder.UseTls); @@ -34,7 +36,8 @@ public void InitDefaultValues_WhenEmptyConstructorInvoke_ReturnDefaultConnection Assert.Equal("UseTls=False;Host=localhost;Port=2136;Database=/local;User=;Password=;ConnectTimeout=5;" + "KeepAlivePingDelay=10;KeepAlivePingTimeout=10;EnableMultipleHttp2Connections=False;" + - $"MaxSendMessageSize={MessageSize};MaxReceiveMessageSize={MessageSize};DisableDiscovery=False", + $"MaxSendMessageSize={MessageSize};MaxReceiveMessageSize={MessageSize};PooledConnectionIdleTimeout=60;" + + "PooledConnectionLifetime=300;DisableDiscovery=False", ydbConnectionStringBuilder.GrpcConnectionString); } @@ -86,7 +89,8 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection Assert.True(ydbConnectionStringBuilder.EnableImplicitSession); Assert.Equal("UseTls=True;Host=server;Port=2135;Database=/my/path;User=Kirill;Password=;ConnectTimeout=30;" + "KeepAlivePingDelay=30;KeepAlivePingTimeout=60;EnableMultipleHttp2Connections=True;" + - "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;DisableDiscovery=True", + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;PooledConnectionIdleTimeout=60;" + + "PooledConnectionLifetime=300;DisableDiscovery=True", ydbConnectionStringBuilder.GrpcConnectionString); } @@ -98,7 +102,8 @@ public void Host_WhenSetInProperty_ReturnUpdatedConnectionString() Assert.Equal( "UseTls=False;Host=server;Port=2135;Database=/my/path;User=Kirill;Password=;ConnectTimeout=5;" + "KeepAlivePingDelay=10;KeepAlivePingTimeout=10;EnableMultipleHttp2Connections=False;" + - $"MaxSendMessageSize={MessageSize};MaxReceiveMessageSize={MessageSize};DisableDiscovery=False", + $"MaxSendMessageSize={MessageSize};MaxReceiveMessageSize={MessageSize};PooledConnectionIdleTimeout=60;" + + "PooledConnectionLifetime=300;DisableDiscovery=False", ydbConnectionStringBuilder.GrpcConnectionString); Assert.Equal("server", ydbConnectionStringBuilder.Host); ydbConnectionStringBuilder.Host = "new_server"; @@ -106,7 +111,8 @@ public void Host_WhenSetInProperty_ReturnUpdatedConnectionString() Assert.Equal( "UseTls=False;Host=new_server;Port=2135;Database=/my/path;User=Kirill;Password=;ConnectTimeout=5;" + "KeepAlivePingDelay=10;KeepAlivePingTimeout=10;EnableMultipleHttp2Connections=False;" + - $"MaxSendMessageSize={MessageSize};MaxReceiveMessageSize={MessageSize};DisableDiscovery=False", + $"MaxSendMessageSize={MessageSize};MaxReceiveMessageSize={MessageSize};PooledConnectionIdleTimeout=60;" + + "PooledConnectionLifetime=300;DisableDiscovery=False", ydbConnectionStringBuilder.GrpcConnectionString); Assert.Equal("Host=new_server;Port=2135;Database=/my/path;User=Kirill", ydbConnectionStringBuilder.ConnectionString);