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);