From a0927a4c59ba3446030ef4b71f5c2045ff273711 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Tue, 12 Aug 2025 19:48:38 +0300 Subject: [PATCH 01/48] feat: add EnableImplicitSession flag support with parsing tests --- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 30 ++++++++++--- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 5 +++ .../src/Ado/YdbConnectionStringBuilder.cs | 16 +++++++ .../YdbConnectionStringBuilderTests.cs | 6 ++- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 44 +++++++++++++++++++ 5 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 2025991e..b78cbbad 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -215,11 +215,31 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha throw new InvalidOperationException("Transaction mismatched! (Maybe using another connection)"); } - var ydbDataReader = await YdbDataReader.CreateYdbDataReader( - await YdbConnection.Session - .ExecuteQuery(preparedSql.ToString(), ydbParameters, execSettings, transaction?.TransactionControl), - YdbConnection.OnNotSuccessStatusCode, transaction, cancellationToken - ); + var useImplicit = YdbConnection.EnableImplicitSession && transaction is null; + var session = YdbConnection.GetExecutionSession(useImplicit); + + YdbDataReader ydbDataReader; + try + { + var execResult = await session.ExecuteQuery( + preparedSql.ToString(), + ydbParameters, + execSettings, + transaction?.TransactionControl + ); + + ydbDataReader = await YdbDataReader.CreateYdbDataReader( + execResult, + YdbConnection.OnNotSuccessStatusCode, + transaction, + cancellationToken + ); + } + finally + { + if (useImplicit) + session.Close(); + } YdbConnection.LastReader = ydbDataReader; YdbConnection.LastCommand = CommandText; diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index de1e3c4d..7b33f105 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -38,6 +38,11 @@ internal ISession Session } private ISession _session = null!; + + internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; + + internal ISession GetExecutionSession(bool useImplicit) + => useImplicit ? new ImplicitSession(Session.Driver) : Session; public YdbConnection() { diff --git a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs index d135d1ea..fc31e163 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs @@ -41,6 +41,7 @@ private void InitDefaultValues() _maxReceiveMessageSize = GrpcDefaultSettings.MaxReceiveMessageSize; _disableDiscovery = GrpcDefaultSettings.DisableDiscovery; _disableServerBalancer = false; + _enableImplicitSession = false; } public string Host @@ -315,6 +316,18 @@ public int CreateSessionTimeout private int _createSessionTimeout; + public bool EnableImplicitSession + { + get => _enableImplicitSession; + set + { + _enableImplicitSession = value; + SaveValue(nameof(EnableImplicitSession), value); + } + } + + private bool _enableImplicitSession; + public ILoggerFactory? LoggerFactory { get; init; } public ICredentialsProvider? CredentialsProvider { get; init; } @@ -491,6 +504,9 @@ static YdbConnectionOption() AddOption(new YdbConnectionOption(BoolExtractor, (builder, disableServerBalancer) => builder.DisableServerBalancer = disableServerBalancer), "DisableServerBalancer", "Disable Server Balancer"); + AddOption(new YdbConnectionOption(BoolExtractor, + (builder, enableImplicit) => builder.EnableImplicitSession = enableImplicit), + "EnableImplicitSession", "ImplicitSession"); } private static void AddOption(YdbConnectionOption option, params string[] keys) 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 736b1e96..33a9aa15 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs @@ -28,6 +28,7 @@ public void InitDefaultValues_WhenEmptyConstructorInvoke_ReturnDefaultConnection Assert.False(ydbConnectionStringBuilder.DisableDiscovery); Assert.False(ydbConnectionStringBuilder.DisableServerBalancer); Assert.False(ydbConnectionStringBuilder.UseTls); + Assert.False(ydbConnectionStringBuilder.EnableImplicitSession); } [Fact] @@ -50,7 +51,7 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=true;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=true;DisableServerBalancer=true;" + "DisableDiscovery=true;DisableServerBalancer=true;EnableImplicitSession=true;" ); Assert.Equal(2135, connectionString.Port); @@ -74,9 +75,10 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=True;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=True;DisableServerBalancer=True", connectionString.ConnectionString); + "DisableDiscovery=True;DisableServerBalancer=True;EnableImplicitSession=True", connectionString.ConnectionString); Assert.True(connectionString.DisableDiscovery); Assert.True(connectionString.DisableServerBalancer); + Assert.True(connectionString.EnableImplicitSession); } [Fact] diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index 3001816a..4a5719c4 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -483,4 +483,48 @@ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() await Assert.ThrowsAsync(async () => { await importer.FlushAsync(); }); } + + [Fact] + public void EnableImplicitSession_WhenSetViaPrimaryKey_ParsesAndAppearsInConnectionString() + { + var csb = new YdbConnectionStringBuilder("EnableImplicitSession=true;Host=server;Port=2135;"); + Assert.True(csb.EnableImplicitSession); + + Assert.Contains("EnableImplicitSession=True", csb.ConnectionString); + Assert.Contains("Host=server", csb.ConnectionString); + Assert.Contains("Port=2135", csb.ConnectionString); + } + + [Fact] + public void EnableImplicitSession_WhenSetViaAlias_ParsesAndNormalizesKey() + { + var csb = new YdbConnectionStringBuilder("ImplicitSession=on;Host=server;Port=2135;"); + Assert.True(csb.EnableImplicitSession); + + var s = csb.ConnectionString; + + Assert.Contains("EnableImplicitSession=True", s); + + var parts = s.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.DoesNotContain(parts, p => p.StartsWith("ImplicitSession=", StringComparison.OrdinalIgnoreCase)); + + Assert.Contains("Host=server", s); + Assert.Contains("Port=2135", s); + } + + [Theory] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("on", true)] + [InlineData("1", true)] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("off", false)] + [InlineData("0", false)] + public void EnableImplicitSession_StringBooleanVariants_AreParsed(string value, bool expected) + { + var csb = new YdbConnectionStringBuilder($"EnableImplicitSession={value};"); + Assert.Equal(expected, csb.EnableImplicitSession); + Assert.Contains($"EnableImplicitSession={(expected ? "True" : "False")}", csb.ConnectionString); + } } From f18fbf715658ea02a9223eadcecc0e0a98c3c388 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Tue, 12 Aug 2025 19:48:38 +0300 Subject: [PATCH 02/48] feat: add EnableImplicitSession flag support with parsing tests --- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 5 +++ .../src/Ado/YdbConnectionStringBuilder.cs | 16 +++++++ .../YdbConnectionStringBuilderTests.cs | 6 ++- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 44 +++++++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index fe69763f..615991e4 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -38,6 +38,11 @@ internal ISession Session } private ISession _session = null!; + + internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; + + internal ISession GetExecutionSession(bool useImplicit) + => useImplicit ? new ImplicitSession(Session.Driver) : Session; public YdbConnection() { diff --git a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs index d135d1ea..fc31e163 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs @@ -41,6 +41,7 @@ private void InitDefaultValues() _maxReceiveMessageSize = GrpcDefaultSettings.MaxReceiveMessageSize; _disableDiscovery = GrpcDefaultSettings.DisableDiscovery; _disableServerBalancer = false; + _enableImplicitSession = false; } public string Host @@ -315,6 +316,18 @@ public int CreateSessionTimeout private int _createSessionTimeout; + public bool EnableImplicitSession + { + get => _enableImplicitSession; + set + { + _enableImplicitSession = value; + SaveValue(nameof(EnableImplicitSession), value); + } + } + + private bool _enableImplicitSession; + public ILoggerFactory? LoggerFactory { get; init; } public ICredentialsProvider? CredentialsProvider { get; init; } @@ -491,6 +504,9 @@ static YdbConnectionOption() AddOption(new YdbConnectionOption(BoolExtractor, (builder, disableServerBalancer) => builder.DisableServerBalancer = disableServerBalancer), "DisableServerBalancer", "Disable Server Balancer"); + AddOption(new YdbConnectionOption(BoolExtractor, + (builder, enableImplicit) => builder.EnableImplicitSession = enableImplicit), + "EnableImplicitSession", "ImplicitSession"); } private static void AddOption(YdbConnectionOption option, params string[] keys) 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 736b1e96..33a9aa15 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs @@ -28,6 +28,7 @@ public void InitDefaultValues_WhenEmptyConstructorInvoke_ReturnDefaultConnection Assert.False(ydbConnectionStringBuilder.DisableDiscovery); Assert.False(ydbConnectionStringBuilder.DisableServerBalancer); Assert.False(ydbConnectionStringBuilder.UseTls); + Assert.False(ydbConnectionStringBuilder.EnableImplicitSession); } [Fact] @@ -50,7 +51,7 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=true;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=true;DisableServerBalancer=true;" + "DisableDiscovery=true;DisableServerBalancer=true;EnableImplicitSession=true;" ); Assert.Equal(2135, connectionString.Port); @@ -74,9 +75,10 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=True;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=True;DisableServerBalancer=True", connectionString.ConnectionString); + "DisableDiscovery=True;DisableServerBalancer=True;EnableImplicitSession=True", connectionString.ConnectionString); Assert.True(connectionString.DisableDiscovery); Assert.True(connectionString.DisableServerBalancer); + Assert.True(connectionString.EnableImplicitSession); } [Fact] diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index 89413315..232c5029 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -477,4 +477,48 @@ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() await Assert.ThrowsAsync(async () => { await importer.FlushAsync(); }); } + + [Fact] + public void EnableImplicitSession_WhenSetViaPrimaryKey_ParsesAndAppearsInConnectionString() + { + var csb = new YdbConnectionStringBuilder("EnableImplicitSession=true;Host=server;Port=2135;"); + Assert.True(csb.EnableImplicitSession); + + Assert.Contains("EnableImplicitSession=True", csb.ConnectionString); + Assert.Contains("Host=server", csb.ConnectionString); + Assert.Contains("Port=2135", csb.ConnectionString); + } + + [Fact] + public void EnableImplicitSession_WhenSetViaAlias_ParsesAndNormalizesKey() + { + var csb = new YdbConnectionStringBuilder("ImplicitSession=on;Host=server;Port=2135;"); + Assert.True(csb.EnableImplicitSession); + + var s = csb.ConnectionString; + + Assert.Contains("EnableImplicitSession=True", s); + + var parts = s.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.DoesNotContain(parts, p => p.StartsWith("ImplicitSession=", StringComparison.OrdinalIgnoreCase)); + + Assert.Contains("Host=server", s); + Assert.Contains("Port=2135", s); + } + + [Theory] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("on", true)] + [InlineData("1", true)] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("off", false)] + [InlineData("0", false)] + public void EnableImplicitSession_StringBooleanVariants_AreParsed(string value, bool expected) + { + var csb = new YdbConnectionStringBuilder($"EnableImplicitSession={value};"); + Assert.Equal(expected, csb.EnableImplicitSession); + Assert.Contains($"EnableImplicitSession={(expected ? "True" : "False")}", csb.ConnectionString); + } } From 5c7cfc9c9ca2ed992bd59d68f27f1b72d48544b3 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Tue, 12 Aug 2025 22:38:56 +0300 Subject: [PATCH 03/48] resolve conflict --- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index c5a5a9d7..7be84fa7 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -217,9 +217,31 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha throw new InvalidOperationException("Transaction mismatched! (Maybe using another connection)"); } - var ydbDataReader = await YdbDataReader.CreateYdbDataReader(await YdbConnection.Session.ExecuteQuery( - preparedSql.ToString(), ydbParameters, execSettings, transaction?.TransactionControl - ), YdbConnection.OnNotSuccessStatusCode, transaction, cancellationToken); + var useImplicit = YdbConnection.EnableImplicitSession && transaction is null; + var session = YdbConnection.GetExecutionSession(useImplicit); + + YdbDataReader ydbDataReader; + try + { + var execResult = await session.ExecuteQuery( + preparedSql.ToString(), + ydbParameters, + execSettings, + transaction?.TransactionControl + ); + + ydbDataReader = await YdbDataReader.CreateYdbDataReader( + execResult, + YdbConnection.OnNotSuccessStatusCode, + transaction, + cancellationToken + ); + } + finally + { + if (useImplicit) + session.Close(); + } YdbConnection.LastReader = ydbDataReader; YdbConnection.LastCommand = CommandText; From eab1450f681dfdf21eb0bc10cdee94cbb8ed2f7a Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Tue, 12 Aug 2025 23:31:21 +0300 Subject: [PATCH 04/48] fix ci --- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 4 ++-- .../YdbConnectionStringBuilderTests.cs | 3 ++- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 18 +++++++++--------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 615991e4..bea91db1 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -38,9 +38,9 @@ internal ISession Session } private ISession _session = null!; - + internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; - + internal ISession GetExecutionSession(bool useImplicit) => useImplicit ? new ImplicitSession(Session.Driver) : Session; 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 33a9aa15..386c71d6 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs @@ -75,7 +75,8 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=True;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=True;DisableServerBalancer=True;EnableImplicitSession=True", connectionString.ConnectionString); + "DisableDiscovery=True;DisableServerBalancer=True;EnableImplicitSession=True", + connectionString.ConnectionString); Assert.True(connectionString.DisableDiscovery); Assert.True(connectionString.DisableServerBalancer); Assert.True(connectionString.EnableImplicitSession); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index 232c5029..bd15fd33 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -477,7 +477,7 @@ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() await Assert.ThrowsAsync(async () => { await importer.FlushAsync(); }); } - + [Fact] public void EnableImplicitSession_WhenSetViaPrimaryKey_ParsesAndAppearsInConnectionString() { @@ -485,10 +485,10 @@ public void EnableImplicitSession_WhenSetViaPrimaryKey_ParsesAndAppearsInConnect Assert.True(csb.EnableImplicitSession); Assert.Contains("EnableImplicitSession=True", csb.ConnectionString); - Assert.Contains("Host=server", csb.ConnectionString); + Assert.Contains("Host=server", csb.ConnectionString); Assert.Contains("Port=2135", csb.ConnectionString); } - + [Fact] public void EnableImplicitSession_WhenSetViaAlias_ParsesAndNormalizesKey() { @@ -507,14 +507,14 @@ public void EnableImplicitSession_WhenSetViaAlias_ParsesAndNormalizesKey() } [Theory] - [InlineData("true", true)] - [InlineData("True", true)] - [InlineData("on", true)] - [InlineData("1", true)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("on", true)] + [InlineData("1", true)] [InlineData("false", false)] [InlineData("False", false)] - [InlineData("off", false)] - [InlineData("0", false)] + [InlineData("off", false)] + [InlineData("0", false)] public void EnableImplicitSession_StringBooleanVariants_AreParsed(string value, bool expected) { var csb = new YdbConnectionStringBuilder($"EnableImplicitSession={value};"); From 72c53f39bf4bd6235862c7e1b4caf19b33c908f1 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Thu, 14 Aug 2025 12:45:42 +0300 Subject: [PATCH 05/48] feat: add integration tests and rework implicit session handling via ISessionSource --- src/Ydb.Sdk/src/Ado/PoolManager.cs | 55 ++++-- .../src/Ado/Session/ImplicitSessionSource.cs | 15 ++ src/Ydb.Sdk/src/Ado/YdbConnection.cs | 2 +- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 186 +++++++++++++++--- 4 files changed, 217 insertions(+), 41 deletions(-) create mode 100644 src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index 84606558..6b594060 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -6,7 +6,9 @@ namespace Ydb.Sdk.Ado; internal static class PoolManager { private static readonly SemaphoreSlim SemaphoreSlim = new(1); // async mutex + private static readonly ConcurrentDictionary Pools = new(); + private static readonly ConcurrentDictionary ImplicitPools = new(); internal static async Task GetSession( YdbConnectionStringBuilder settings, @@ -41,29 +43,58 @@ await PoolingSessionFactory.Create(settings), settings } } - internal static async Task ClearPool(string connectionString) + internal static ISession GetImplicitSession(YdbConnectionStringBuilder settings) { - if (Pools.Remove(connectionString, out var sessionPool)) - { - try - { - await SemaphoreSlim.WaitAsync(); + if (ImplicitPools.TryGetValue(settings.ConnectionString, out var ready)) + return ready.OpenSession(CancellationToken.None).GetAwaiter().GetResult(); - await sessionPool.DisposeAsync(); - } - finally + var driver = settings.BuildDriver().GetAwaiter().GetResult(); + ISessionSource source; + + SemaphoreSlim.Wait(); + try + { + if (!ImplicitPools.TryGetValue(settings.ConnectionString, out source)) { - SemaphoreSlim.Release(); + source = new ImplicitSessionSource(driver); + ImplicitPools[settings.ConnectionString] = source; + driver = null; } } + finally + { + SemaphoreSlim.Release(); + if (driver != null) + driver.DisposeAsync().GetAwaiter().GetResult(); + } + + return source.OpenSession(CancellationToken.None).GetAwaiter().GetResult(); + } + + internal static async Task ClearPool(string connectionString) + { + Pools.TryRemove(connectionString, out var pooled); + ImplicitPools.TryRemove(connectionString, out var implicitSrc); + + var tasks = new List(2); + if (pooled != null) tasks.Add(pooled.DisposeAsync().AsTask()); + if (implicitSrc != null) tasks.Add(implicitSrc.DisposeAsync().AsTask()); + + if (tasks.Count > 0) + await Task.WhenAll(tasks); } internal static async Task ClearAllPools() { - var keys = Pools.Keys.ToList(); + var pooled = Pools.ToArray(); + var implicitArr = ImplicitPools.ToArray(); - var tasks = keys.Select(ClearPool).ToList(); + Pools.Clear(); + ImplicitPools.Clear(); + var tasks = new List(pooled.Length + implicitArr.Length); + tasks.AddRange(pooled.Select(kv => kv.Value.DisposeAsync().AsTask())); + tasks.AddRange(implicitArr.Select(kv => kv.Value.DisposeAsync().AsTask())); await Task.WhenAll(tasks); } } diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs new file mode 100644 index 00000000..5adea245 --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -0,0 +1,15 @@ +namespace Ydb.Sdk.Ado.Session; + +internal sealed class ImplicitSessionSource : ISessionSource +{ + private readonly IDriver _driver; + + internal ImplicitSessionSource(IDriver driver) + { + _driver = driver; + } + + public ValueTask OpenSession(CancellationToken cancellationToken) => new(new ImplicitSession(_driver)); + + public async ValueTask DisposeAsync() => await _driver.DisposeAsync(); +} \ No newline at end of file diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index bea91db1..10754d16 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -42,7 +42,7 @@ internal ISession Session internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; internal ISession GetExecutionSession(bool useImplicit) - => useImplicit ? new ImplicitSession(Session.Driver) : Session; + => useImplicit ? PoolManager.GetImplicitSession(ConnectionStringBuilder) : Session; public YdbConnection() { diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index bd15fd33..f51d0b0c 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -479,46 +479,176 @@ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() } [Fact] - public void EnableImplicitSession_WhenSetViaPrimaryKey_ParsesAndAppearsInConnectionString() + public async Task EnableImplicitSession_WhenTrue_AndNoTransaction_UsesImplicitSession() { - var csb = new YdbConnectionStringBuilder("EnableImplicitSession=true;Host=server;Port=2135;"); - Assert.True(csb.EnableImplicitSession); + var cs = ConnectionString + ";EnableImplicitSession=true"; - Assert.Contains("EnableImplicitSession=True", csb.ConnectionString); - Assert.Contains("Host=server", csb.ConnectionString); - Assert.Contains("Port=2135", csb.ConnectionString); + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + + var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1"; + var result = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + Assert.Equal(1L, result); + + var implicitSession = conn.GetExecutionSession(useImplicit: true); + var pooledSession = conn.GetExecutionSession(useImplicit: false); + Assert.NotEqual(implicitSession, pooledSession); } [Fact] - public void EnableImplicitSession_WhenSetViaAlias_ParsesAndNormalizesKey() + public async Task EnableImplicitSession_WhenTrue_ButInsideTransaction_UsesPooledSession() { - var csb = new YdbConnectionStringBuilder("ImplicitSession=on;Host=server;Port=2135;"); - Assert.True(csb.EnableImplicitSession); + var cs = ConnectionString + ";EnableImplicitSession=true"; + + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + + using var tx = conn.BeginTransaction(); + var cmd = conn.CreateCommand(); + cmd.Transaction = tx; + cmd.CommandText = "SELECT 1"; + var result = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + Assert.Equal(1L, result); + + var pooledSession = conn.GetExecutionSession(useImplicit: false); + var implicitSession = conn.GetExecutionSession(useImplicit: true); + + Assert.Equal(pooledSession, conn.Session); + Assert.NotEqual(pooledSession, implicitSession); + } + + [Fact] + public async Task EnableImplicitSession_WhenFalse_AlwaysUsesPooledSession() + { + var cs = ConnectionString + ";EnableImplicitSession=false"; + + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); - var s = csb.ConnectionString; + var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT CAST(1 AS Int64)"; + var result = (long)(await cmd.ExecuteScalarAsync())!; + Assert.Equal(1L, result); - Assert.Contains("EnableImplicitSession=True", s); + var pooledSession = conn.GetExecutionSession(useImplicit: false); + Assert.Equal(pooledSession, conn.Session); + } + + [Fact] + public async Task EnableImplicitSession_DifferentConnectionStrings_HaveDifferentImplicitPools() + { + var cs1 = ConnectionString + ";EnableImplicitSession=true;MinSessionPool=0;DisableDiscovery=false"; + var cs2 = ConnectionString + ";EnableImplicitSession=true;MinSessionPool=1;DisableDiscovery=false"; + + await using var conn1 = new YdbConnection(cs1); + await conn1.OpenAsync(); + var session1 = conn1.GetExecutionSession(useImplicit: true); - var parts = s.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - Assert.DoesNotContain(parts, p => p.StartsWith("ImplicitSession=", StringComparison.OrdinalIgnoreCase)); + await using var conn2 = new YdbConnection(cs2); + await conn2.OpenAsync(); + var session2 = conn2.GetExecutionSession(useImplicit: true); - Assert.Contains("Host=server", s); - Assert.Contains("Port=2135", s); + Assert.NotEqual(session1, session2); } + + [Fact] + public async Task EnableImplicitSession_TwoSequentialCommands_GetDifferentImplicitSessions() + { + var cs = ConnectionString + ";EnableImplicitSession=true"; + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + + var s1 = conn.GetExecutionSession(useImplicit: true); + var s2 = conn.GetExecutionSession(useImplicit: true); - [Theory] - [InlineData("true", true)] - [InlineData("True", true)] - [InlineData("on", true)] - [InlineData("1", true)] - [InlineData("false", false)] - [InlineData("False", false)] - [InlineData("off", false)] - [InlineData("0", false)] - public void EnableImplicitSession_StringBooleanVariants_AreParsed(string value, bool expected) + Assert.NotEqual(s1, s2); + } + + [Fact] + public async Task ClearPool_FireAndForget_DoesNotBlock_And_PoolsRecreate() + { + var csBase = + ConnectionString + + ";UseTls=false" + + ";DisableDiscovery=true" + + ";CreateSessionTimeout=3" + + ";ConnectTimeout=3" + + ";KeepAlivePingDelay=0;KeepAlivePingTimeout=0"; + + var csPooled = csBase; // pooled-пул (без флага) + var csImplicit = csBase + ";EnableImplicitSession=true"; // implicit-пул (с флагом) + + // 1) Прогреваем оба пула (pooled и implicit), чтобы они точно были созданы. + await using (var warmPooled = new YdbConnection(csPooled)) + { + await warmPooled.OpenAsync(); + using var cmd = warmPooled.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); + } + + await using (var warmImplicit = new YdbConnection(csImplicit)) + { + await warmImplicit.OpenAsync(); + using var cmd = warmImplicit.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); + } + + // 2) Вызываем ClearPool для ОБОИХ ключей (по вашей реализации ключи разные). + var clearPooledTask = YdbConnection.ClearPool(new YdbConnection(csPooled)); + var clearImplicitTask = YdbConnection.ClearPool(new YdbConnection(csImplicit)); + + // 3) Убеждаемся, что ClearPool не блокирует — завершается быстро (fail-fast). + // (Если вдруг среда перегружена, можно поднять таймаут до 3–5 секунд.) + var done = await Task.WhenAny(Task.WhenAll(clearPooledTask, clearImplicitTask), Task.Delay(TimeSpan.FromSeconds(2))); + Assert.True(done is not null && done != Task.Delay(TimeSpan.FromSeconds(2)), "ClearPool() must not block."); + + // 4) Проверяем, что пулы корректно пересоздаются после очистки: + // pooled — без флага, implicit — с флагом. + await using (var checkPooled = new YdbConnection(csPooled)) + { + await checkPooled.OpenAsync(); + using var cmd = checkPooled.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); + } + + await using (var checkImplicit = new YdbConnection(csImplicit)) + { + await checkImplicit.OpenAsync(); + using var cmd = checkImplicit.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); + } + } + + [Fact] + public async Task EnableImplicitSession_ParallelQueries_WorkFine() + { + var cs = ConnectionString + ";EnableImplicitSession=true"; + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + + var tasks = Enumerable.Range(0, 16).Select(async _ => + { + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1"; + var v = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + Assert.Equal(1L, v); + }); + await Task.WhenAll(tasks); + } + + [Fact] + public async Task EnableImplicitSession_WithDisableDiscovery_Works() { - var csb = new YdbConnectionStringBuilder($"EnableImplicitSession={value};"); - Assert.Equal(expected, csb.EnableImplicitSession); - Assert.Contains($"EnableImplicitSession={(expected ? "True" : "False")}", csb.ConnectionString); + var cs = ConnectionString + ";EnableImplicitSession=true;DisableDiscovery=true"; + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); } } From 7c35439feca4a4315581aec77eaea310301f03b2 Mon Sep 17 00:00:00 2001 From: Kirill Kurdyukov Date: Wed, 13 Aug 2025 16:25:05 +0300 Subject: [PATCH 06/48] `Warning` -> `Debug` on DeleteSession has been `RpcException` & fixes for SLO (#513) --- .github/workflows/slo.yml | 12 ++--- slo/src/AdoNet/SloTableContext.cs | 7 ++- slo/src/Dapper/SloTableContext.cs | 7 ++- slo/src/Internal/SloTableContext.cs | 44 +++++++++++-------- src/Ydb.Sdk/CHANGELOG.md | 3 +- src/Ydb.Sdk/src/Ado/Session/PoolingSession.cs | 2 +- .../src/Ado/YdbConnectionStringBuilder.cs | 5 +-- 7 files changed, 43 insertions(+), 37 deletions(-) diff --git a/.github/workflows/slo.yml b/.github/workflows/slo.yml index 85c29514..cf4fe1a5 100644 --- a/.github/workflows/slo.yml +++ b/.github/workflows/slo.yml @@ -23,13 +23,13 @@ jobs: include: - workload: AdoNet read_rps: 1000 - write_rps: 1000 + write_rps: 100 - workload: Dapper read_rps: 1000 - write_rps: 1000 + write_rps: 100 - workload: EF - read_rps: 500 - write_rps: 500 + read_rps: 1000 + write_rps: 100 concurrency: group: slo-${{ github.ref }}-${{ matrix.workload }} @@ -64,8 +64,8 @@ jobs: --prom-pgw http://localhost:9091 \ --report-period 250 \ --time 600 \ - --read-rps ${{matrix.read_rps || 1000 }} \ - --write-rps ${{matrix.write_rps || 1000 }} \ + --read-rps ${{ matrix.read_rps }} \ + --write-rps ${{ matrix.write_rps }} \ --read-timeout 1000 \ --write-timeout 1000 diff --git a/slo/src/AdoNet/SloTableContext.cs b/slo/src/AdoNet/SloTableContext.cs index a0ed1d12..7f76439c 100644 --- a/slo/src/AdoNet/SloTableContext.cs +++ b/slo/src/AdoNet/SloTableContext.cs @@ -1,6 +1,5 @@ using System.Data; using Internal; -using Microsoft.Extensions.Logging; using Polly; using Ydb.Sdk; using Ydb.Sdk.Ado; @@ -9,9 +8,9 @@ namespace AdoNet; public class SloTableContext : SloTableContext { - private static readonly AsyncPolicy Policy = Polly.Policy.Handle(exception => exception.IsTransient) - .WaitAndRetryAsync(10, attempt => TimeSpan.FromMilliseconds(attempt * 10), - (e, _, _, _) => { Logger.LogWarning(e, "Failed read / write operation"); }); + private static readonly AsyncPolicy Policy = Polly.Policy + .Handle(exception => exception.IsTransient) + .RetryAsync(10); protected override string Job => "AdoNet"; diff --git a/slo/src/Dapper/SloTableContext.cs b/slo/src/Dapper/SloTableContext.cs index bd801813..b2e8ac99 100644 --- a/slo/src/Dapper/SloTableContext.cs +++ b/slo/src/Dapper/SloTableContext.cs @@ -1,6 +1,5 @@ using Dapper; using Internal; -using Microsoft.Extensions.Logging; using Polly; using Ydb.Sdk; using Ydb.Sdk.Ado; @@ -9,9 +8,9 @@ namespace AdoNet.Dapper; public class SloTableContext : SloTableContext { - private static readonly AsyncPolicy Policy = Polly.Policy.Handle(exception => exception.IsTransient) - .WaitAndRetryAsync(10, attempt => TimeSpan.FromMilliseconds(attempt * 10), - (e, _, _, _) => { Logger.LogWarning(e, "Failed read / write operation"); }); + private static readonly AsyncPolicy Policy = Polly.Policy + .Handle(exception => exception.IsTransient) + .RetryAsync(10); protected override string Job => "Dapper"; diff --git a/slo/src/Internal/SloTableContext.cs b/slo/src/Internal/SloTableContext.cs index c7f1013c..3eea759d 100644 --- a/slo/src/Internal/SloTableContext.cs +++ b/slo/src/Internal/SloTableContext.cs @@ -19,6 +19,8 @@ public interface ISloContext public abstract class SloTableContext : ISloContext { + private const int IntervalMs = 100; + protected static readonly ILogger Logger = ISloContext.Factory.CreateLogger>(); private volatile int _maxId; @@ -95,11 +97,13 @@ public async Task Run(RunConfig runConfig) var writeLimiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions { - Window = TimeSpan.FromMilliseconds(100), PermitLimit = runConfig.WriteRps / 10, QueueLimit = int.MaxValue + Window = TimeSpan.FromMilliseconds(IntervalMs), PermitLimit = runConfig.WriteRps / 10, + QueueLimit = int.MaxValue }); var readLimiter = new FixedWindowRateLimiter(new FixedWindowRateLimiterOptions { - Window = TimeSpan.FromMilliseconds(100), PermitLimit = runConfig.ReadRps / 10, QueueLimit = int.MaxValue + Window = TimeSpan.FromMilliseconds(IntervalMs), PermitLimit = runConfig.ReadRps / 10, + QueueLimit = int.MaxValue }); var cancellationTokenSource = new CancellationTokenSource(); @@ -124,7 +128,7 @@ public async Task Run(RunConfig runConfig) Logger.LogInformation("Run task is finished"); return; - Task ShootingTask(RateLimiter rateLimitPolicy, string operationType, + async Task ShootingTask(RateLimiter rateLimitPolicy, string operationType, Func> action) { var metricFactory = Metrics.WithLabels(new Dictionary @@ -193,21 +197,22 @@ Task ShootingTask(RateLimiter rateLimitPolicy, string operationType, ["error_type"] ); - // ReSharper disable once MethodSupportsCancellation - return Task.Run(async () => + var workJobs = new List(); + + for (var i = 0; i < 10; i++) { - while (!cancellationTokenSource.Token.IsCancellationRequested) + workJobs.Add(Task.Run(async () => { - using var lease = await rateLimitPolicy - .AcquireAsync(cancellationToken: cancellationTokenSource.Token); - - if (!lease.IsAcquired) + while (!cancellationTokenSource.Token.IsCancellationRequested) { - continue; - } + using var lease = await rateLimitPolicy + .AcquireAsync(cancellationToken: cancellationTokenSource.Token); + + if (!lease.IsAcquired) + { + await Task.Delay(Random.Shared.Next(IntervalMs / 2), cancellationTokenSource.Token); + } - _ = Task.Run(async () => - { try { pendingOperations.Inc(); @@ -235,11 +240,14 @@ Task ShootingTask(RateLimiter rateLimitPolicy, string operationType, { Logger.LogError(e, "Fail operation!"); } - }, cancellationTokenSource.Token); - } + } + }, cancellationTokenSource.Token)); + } + + // ReSharper disable once MethodSupportsCancellation + await Task.WhenAll(workJobs); - Logger.LogInformation("{ShootingName} shooting is stopped", operationType); - }); + Logger.LogInformation("{ShootingName} shooting is stopped", operationType); } } diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index f8f91a29..0cad865f 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,5 @@ +- Dev: LogLevel `Warning` -> `Debug` on DeleteSession has been `RpcException`. + ## v0.22.0 - Added `YdbDbType` property to `YdbParameter`, allowing to explicitly specify YDB-specific data types for parameter mapping. @@ -21,7 +23,6 @@ - Added new ADO.NET options: - `MinSessionPool`: The minimum connection pool size. - `SessionIdleTimeout`: The time (in seconds) to wait before closing idle session in the pool if the count of all sessions exceeds `MinSessionPool`. - - `SessionPruningInterval`: How many seconds the pool waits before attempting to prune idle sessions (see `SessionIdleTimeout`). - Fixed bug `Reader`: unhandled exception in `TryReadRequestBytes(long bytes)`. - Handle `YdbException` on `DeleteSession`. - Do not invoke `DeleteSession` if the session is not active. diff --git a/src/Ydb.Sdk/src/Ado/Session/PoolingSession.cs b/src/Ydb.Sdk/src/Ado/Session/PoolingSession.cs index 3a7cac71..2849a61f 100644 --- a/src/Ydb.Sdk/src/Ado/Session/PoolingSession.cs +++ b/src/Ydb.Sdk/src/Ado/Session/PoolingSession.cs @@ -235,7 +235,7 @@ internal override async Task DeleteSession() } catch (Exception e) { - _logger.LogWarning(e, "Error occurred while deleting session[{SessionId}] (NodeId = {NodeId})", + _logger.LogDebug(e, "Error occurred while deleting session[{SessionId}] (NodeId = {NodeId})", SessionId, NodeId); } } diff --git a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs index fc31e163..b2305b38 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Ydb.Sdk.Auth; -using Ydb.Sdk.Pool; using Ydb.Sdk.Transport; namespace Ydb.Sdk.Ado; @@ -29,8 +28,8 @@ private void InitDefaultValues() _port = 2136; _database = "/local"; _minSessionPool = 0; - _maxSessionPool = SessionPoolDefaultSettings.MaxSessionPool; - _createSessionTimeout = SessionPoolDefaultSettings.CreateSessionTimeoutSeconds; + _maxSessionPool = 100; + _createSessionTimeout = 5; _sessionIdleTimeout = 300; _useTls = false; _connectTimeout = GrpcDefaultSettings.ConnectTimeoutSeconds; From 4dfa0bd909ed5aa45a758da6638790ce78255417 Mon Sep 17 00:00:00 2001 From: Kirill Kurdyukov Date: Mon, 18 Aug 2025 16:25:31 +0300 Subject: [PATCH 07/48] Feat: Implement `YdbRetryPolicy` with AWS-inspired Exponential Backoff and Jitter (#515) --- examples/Ydb.Sdk.AdoNet.QuickStart/Program.cs | 3 +- .../Storage/Internal/YdbDatabaseCreator.cs | 2 +- src/Ydb.Sdk/CHANGELOG.md | 1 + src/Ydb.Sdk/src/Ado/Internal/Random.cs | 13 ++ .../src/Ado/Internal/StatusCodeUtils.cs | 18 ++- .../src/Ado/RetryPolicy/IRetryPolicy.cs | 6 + .../src/Ado/RetryPolicy/YdbRetryPolicy.cs | 71 ++++++++ .../Ado/RetryPolicy/YdbRetryPolicyConfig.cs | 18 +++ src/Ydb.Sdk/src/Ado/YdbConnection.cs | 35 ++-- src/Ydb.Sdk/src/Ado/YdbException.cs | 7 +- src/Ydb.Sdk/src/Pool/EndpointPool.cs | 21 +-- .../StressLoadTank.cs | 5 +- .../Pool/EndpointPoolTests.cs | 5 +- .../RetryPolicy/YdbRetryPolicyTests.cs | 151 ++++++++++++++++++ 14 files changed, 305 insertions(+), 51 deletions(-) create mode 100644 src/Ydb.Sdk/src/Ado/Internal/Random.cs create mode 100644 src/Ydb.Sdk/src/Ado/RetryPolicy/IRetryPolicy.cs create mode 100644 src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs create mode 100644 src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicyConfig.cs create mode 100644 src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/RetryPolicy/YdbRetryPolicyTests.cs diff --git a/examples/Ydb.Sdk.AdoNet.QuickStart/Program.cs b/examples/Ydb.Sdk.AdoNet.QuickStart/Program.cs index bba59fd5..fb0a5d9d 100644 --- a/examples/Ydb.Sdk.AdoNet.QuickStart/Program.cs +++ b/examples/Ydb.Sdk.AdoNet.QuickStart/Program.cs @@ -364,7 +364,8 @@ ORDER BY -- Sorting of the results. while (await ydbDataReader.ReadAsync()) { - _logger.LogInformation("season_title: {}, series_title: {}, series_id: {}, season_id: {}", + _logger.LogInformation("season_title: {SeasonTitle}, series_title: {SeriesTitle}, " + + "series_id: {SeriesId}, season_id: {SeasonId}", ydbDataReader.GetString("season_title"), ydbDataReader.GetString("series_title"), ydbDataReader.GetUint64(2), ydbDataReader.GetUint64(3)); } diff --git a/src/EFCore.Ydb/src/Storage/Internal/YdbDatabaseCreator.cs b/src/EFCore.Ydb/src/Storage/Internal/YdbDatabaseCreator.cs index 28012d85..9d502404 100644 --- a/src/EFCore.Ydb/src/Storage/Internal/YdbDatabaseCreator.cs +++ b/src/EFCore.Ydb/src/Storage/Internal/YdbDatabaseCreator.cs @@ -78,7 +78,7 @@ public override async Task DeleteAsync(CancellationToken cancellationToken = def .GetSchemaAsync("Tables", [null, "TABLE"], cancellationToken); var dropTableOperations = (from DataRow row in dataTable.Rows - select new DropTableOperation { Name = row["table_name"].ToString() }).ToList(); + select new DropTableOperation { Name = row["table_name"].ToString()! }).ToList(); await Dependencies.MigrationCommandExecutor.ExecuteNonQueryAsync(Dependencies.MigrationsSqlGenerator .Generate(dropTableOperations), connection, cancellationToken); diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index 0cad865f..b44c4670 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,4 @@ +- Feat: Implement `YdbRetryPolicy` with AWS-inspired Exponential Backoff and Jitter. - Dev: LogLevel `Warning` -> `Debug` on DeleteSession has been `RpcException`. ## v0.22.0 diff --git a/src/Ydb.Sdk/src/Ado/Internal/Random.cs b/src/Ydb.Sdk/src/Ado/Internal/Random.cs new file mode 100644 index 00000000..737457be --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/Internal/Random.cs @@ -0,0 +1,13 @@ +namespace Ydb.Sdk.Ado.Internal; + +public interface IRandom +{ + public int Next(int maxValue); +} + +internal class ThreadLocalRandom : IRandom +{ + internal static readonly ThreadLocalRandom Instance = new(); + + public int Next(int maxValue) => Random.Shared.Next(maxValue); +} diff --git a/src/Ydb.Sdk/src/Ado/Internal/StatusCodeUtils.cs b/src/Ydb.Sdk/src/Ado/Internal/StatusCodeUtils.cs index 57073ccb..6598d9f9 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/StatusCodeUtils.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/StatusCodeUtils.cs @@ -16,11 +16,25 @@ public static class StatusCodeUtils internal static StatusCode Code(this StatusIds.Types.StatusCode statusCode) => Enum.IsDefined(typeof(StatusCode), (int)statusCode) ? (StatusCode)statusCode : StatusCode.Unavailable; - internal static bool IsNotSuccess(this StatusIds.Types.StatusCode code) => - code != StatusIds.Types.StatusCode.Success; + internal static bool IsNotSuccess(this StatusIds.Types.StatusCode statusCode) => + statusCode != StatusIds.Types.StatusCode.Success; internal static string ToMessage(this StatusCode statusCode, IReadOnlyList issueMessages) => issueMessages.Count == 0 ? $"Status: {statusCode}" : $"Status: {statusCode}, Issues:{Environment.NewLine}{issueMessages.IssuesToString()}"; + + internal static bool IsTransient(this StatusCode statusCode) => statusCode is + StatusCode.BadSession or + StatusCode.SessionBusy or + StatusCode.Aborted or + StatusCode.Unavailable or + StatusCode.Overloaded or + StatusCode.SessionExpired or + StatusCode.ClientTransportResourceExhausted; + + internal static bool IsTransientWhenIdempotent(this StatusCode statusCode) => statusCode.IsTransient() || + statusCode is StatusCode.Undetermined or + StatusCode.ClientTransportUnknown or + StatusCode.ClientTransportUnavailable; } diff --git a/src/Ydb.Sdk/src/Ado/RetryPolicy/IRetryPolicy.cs b/src/Ydb.Sdk/src/Ado/RetryPolicy/IRetryPolicy.cs new file mode 100644 index 00000000..84d977ef --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/RetryPolicy/IRetryPolicy.cs @@ -0,0 +1,6 @@ +namespace Ydb.Sdk.Ado.RetryPolicy; + +public interface IRetryPolicy +{ + public TimeSpan? GetNextDelay(YdbException ydbException, int attempt); +} diff --git a/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs new file mode 100644 index 00000000..b9f96abd --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicy.cs @@ -0,0 +1,71 @@ +using Ydb.Sdk.Ado.Internal; + +namespace Ydb.Sdk.Ado.RetryPolicy; + +/// +/// See AWS paper +/// +public class YdbRetryPolicy : IRetryPolicy +{ + public static readonly YdbRetryPolicy Default = new(YdbRetryPolicyConfig.Default); + + private readonly int _maxAttempt; + private readonly int _fastBackoffBaseMs; + private readonly int _slowBackoffBaseMs; + private readonly int _fastCeiling; + private readonly int _slowCeiling; + private readonly int _fastCapBackoffMs; + private readonly int _slowCapBackoffMs; + private readonly bool _enableRetryIdempotence; + private readonly IRandom _random; + + public YdbRetryPolicy(YdbRetryPolicyConfig config) + { + _maxAttempt = config.MaxAttempt; + _fastBackoffBaseMs = config.FastBackoffBaseMs; + _slowBackoffBaseMs = config.SlowBackoffBaseMs; + _fastCeiling = (int)Math.Ceiling(Math.Log(config.FastCapBackoffMs + 1, 2)); + _slowCeiling = (int)Math.Ceiling(Math.Log(config.SlowCapBackoffMs + 1, 2)); + _fastCapBackoffMs = config.FastCapBackoffMs; + _slowCapBackoffMs = config.SlowCapBackoffMs; + _enableRetryIdempotence = config.EnableRetryIdempotence; + _random = ThreadLocalRandom.Instance; + } + + internal YdbRetryPolicy(YdbRetryPolicyConfig config, IRandom random) : this(config) + { + _random = random; + } + + public TimeSpan? GetNextDelay(YdbException ydbException, int attempt) + { + if (attempt >= _maxAttempt || (!_enableRetryIdempotence && !ydbException.IsTransient)) + return null; + + return ydbException.Code switch + { + StatusCode.BadSession or StatusCode.SessionBusy => TimeSpan.Zero, + StatusCode.Aborted or StatusCode.Undetermined => + FullJitter(_fastBackoffBaseMs, _fastCapBackoffMs, _fastCeiling, attempt, _random), + StatusCode.Unavailable or StatusCode.ClientTransportUnknown or StatusCode.ClientTransportUnavailable => + EqualJitter(_fastBackoffBaseMs, _fastCapBackoffMs, _fastCeiling, attempt, _random), + StatusCode.Overloaded or StatusCode.ClientTransportResourceExhausted => + EqualJitter(_slowBackoffBaseMs, _slowCapBackoffMs, _slowCeiling, attempt, _random), + _ => null + }; + } + + private static TimeSpan FullJitter(int backoffBaseMs, int capMs, int ceiling, int attempt, IRandom random) => + TimeSpan.FromMilliseconds(random.Next(CalculateBackoff(backoffBaseMs, capMs, ceiling, attempt) + 1)); + + private static TimeSpan EqualJitter(int backoffBaseMs, int capMs, int ceiling, int attempt, IRandom random) + { + var calculatedBackoff = CalculateBackoff(backoffBaseMs, capMs, ceiling, attempt); + var temp = calculatedBackoff / 2; + + return TimeSpan.FromMilliseconds(temp + calculatedBackoff % 2 + random.Next(temp + 1)); + } + + private static int CalculateBackoff(int backoffBaseMs, int capMs, int ceiling, int attempt) => + Math.Min(backoffBaseMs * (1 << Math.Min(ceiling, attempt)), capMs); +} diff --git a/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicyConfig.cs b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicyConfig.cs new file mode 100644 index 00000000..2e6950bc --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/RetryPolicy/YdbRetryPolicyConfig.cs @@ -0,0 +1,18 @@ +namespace Ydb.Sdk.Ado.RetryPolicy; + +public class YdbRetryPolicyConfig +{ + public static readonly YdbRetryPolicyConfig Default = new(); + + public int MaxAttempt { get; init; } = 10; + + public int FastBackoffBaseMs { get; init; } = 5; + + public int SlowBackoffBaseMs { get; init; } = 50; + + public int FastCapBackoffMs { get; init; } = 500; + + public int SlowCapBackoffMs { get; init; } = 5_000; + + public bool EnableRetryIdempotence { get; init; } = false; +} diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 10754d16..7815d307 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -58,23 +58,6 @@ public YdbConnection(YdbConnectionStringBuilder connectionStringBuilder) ConnectionStringBuilder = connectionStringBuilder; } - public IBulkUpsertImporter BeginBulkUpsertImport( - string name, - IReadOnlyList columns, - CancellationToken cancellationToken = default) - { - ThrowIfConnectionClosed(); - if (CurrentTransaction is { Completed: false }) - throw new InvalidOperationException("BulkUpsert cannot be used inside an active transaction."); - - var database = ConnectionStringBuilder.Database.TrimEnd('/'); - var tablePath = name.StartsWith(database) ? name : $"{database}/{name}"; - - var maxBytes = ConnectionStringBuilder.MaxSendMessageSize; - - return new BulkUpsertImporter(Session.Driver, tablePath, columns, maxBytes, cancellationToken); - } - protected override YdbTransaction BeginDbTransaction(IsolationLevel isolationLevel) { ThrowIfConnectionClosed(); @@ -284,4 +267,22 @@ public override async ValueTask DisposeAsync() /// to their pool. /// public static Task ClearAllPools() => PoolManager.ClearAllPools(); + + public IBulkUpsertImporter BeginBulkUpsertImport( + string name, + IReadOnlyList columns, + CancellationToken cancellationToken = default) + { + ThrowIfConnectionClosed(); + + if (CurrentTransaction is { Completed: false }) + throw new InvalidOperationException("BulkUpsert cannot be used inside an active transaction."); + + var database = ConnectionStringBuilder.Database.TrimEnd('/'); + var tablePath = name.StartsWith(database) ? name : $"{database}/{name}"; + + var maxBytes = ConnectionStringBuilder.MaxSendMessageSize; + + return new BulkUpsertImporter(Session.Driver, tablePath, columns, maxBytes, cancellationToken); + } } diff --git a/src/Ydb.Sdk/src/Ado/YdbException.cs b/src/Ydb.Sdk/src/Ado/YdbException.cs index a419ad34..aed5f07b 100644 --- a/src/Ydb.Sdk/src/Ado/YdbException.cs +++ b/src/Ydb.Sdk/src/Ado/YdbException.cs @@ -18,7 +18,6 @@ internal YdbException(RpcException e) : this(e.Status.Code(), "Transport RPC cal internal static YdbException FromServer(StatusIds.Types.StatusCode statusCode, IReadOnlyList issues) { var code = statusCode.Code(); - var message = code.ToMessage(issues); return new YdbException(code, message); @@ -28,10 +27,8 @@ internal YdbException(StatusCode statusCode, string message, Exception? innerExc : base(message, innerException) { Code = statusCode; - var policy = RetrySettings.DefaultInstance.GetRetryRule(statusCode).Policy; - - IsTransient = policy == RetryPolicy.Unconditional; - IsTransientWhenIdempotent = policy != RetryPolicy.None; + IsTransient = statusCode.IsTransient(); + IsTransientWhenIdempotent = statusCode.IsTransientWhenIdempotent(); // TODO: Add SQLSTATE message with order with https://en.wikipedia.org/wiki/SQLSTATE } diff --git a/src/Ydb.Sdk/src/Pool/EndpointPool.cs b/src/Ydb.Sdk/src/Pool/EndpointPool.cs index feff114e..6f4d8586 100644 --- a/src/Ydb.Sdk/src/Pool/EndpointPool.cs +++ b/src/Ydb.Sdk/src/Pool/EndpointPool.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using Microsoft.Extensions.Logging; +using Ydb.Sdk.Ado.Internal; namespace Ydb.Sdk.Pool; @@ -152,23 +153,3 @@ private record PriorityEndpoint(string Endpoint) } public record EndpointSettings(long NodeId, string Endpoint, string LocationDc); - -public interface IRandom -{ - public int Next(int maxValue); -} - -internal class ThreadLocalRandom : IRandom -{ - internal static readonly ThreadLocalRandom Instance = new(); - - [ThreadStatic] private static Random? _random; - - private static Random ThreadStaticRandom => _random ??= new Random(); - - private ThreadLocalRandom() - { - } - - public int Next(int maxValue) => ThreadStaticRandom.Next(maxValue); -} diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Stress.Loader/StressLoadTank.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Stress.Loader/StressLoadTank.cs index 8c28687e..1e6ca161 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Stress.Loader/StressLoadTank.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Stress.Loader/StressLoadTank.cs @@ -37,13 +37,12 @@ Starting YDB ADO.NET Stress Test Tank Peak RPS: {PeakRps} Medium RPS: {MediumRps} Min RPS: {MinRps} - Load Pattern: Peak({PeakDuration}s) -> Medium({MediumDuration}s) -> Min({MinDuration}s) -> Medium({MediumDuration}s) + Load Pattern: Peak({PeakDuration}s) -> Medium({MediumDuration}s) -> Min({MinDuration}s) Total Test Time: {TotalTime}s Test Query: {TestQuery} """, _config.PeakRps, _config.MediumRps, _config.MinRps, _config.PeakDurationSeconds, - _config.MediumDurationSeconds, _config.MinDurationSeconds, _config.MediumDurationSeconds, - _config.TotalTestTimeSeconds, _config.TestQuery + _config.MediumDurationSeconds, _config.MinDurationSeconds, _config.TotalTestTimeSeconds, _config.TestQuery ); var ctsRunJob = new CancellationTokenSource(); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Pool/EndpointPoolTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Pool/EndpointPoolTests.cs index 9840b761..fbd0adac 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Pool/EndpointPoolTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Pool/EndpointPoolTests.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using Moq; using Xunit; +using Ydb.Sdk.Ado.Internal; using Ydb.Sdk.Ado.Tests.Utils; using Ydb.Sdk.Pool; @@ -112,7 +113,7 @@ public void PessimizeEndpoint_Reset_WhenPessimizedMajorityNodesThenResetAndAddNe listNewEndpointSettings.Add(new EndpointSettings(6, "n6.ydb.tech", "VLA")); listNewEndpointSettings.Add(new EndpointSettings(7, "n7.ydb.tech", "MAN")); - _endpointPool.Reset(listNewEndpointSettings.ToImmutableArray()); + _endpointPool.Reset([..listNewEndpointSettings]); for (var it = 0; it < listNewEndpointSettings.Count; it++) { @@ -141,7 +142,7 @@ public void PessimizeEndpoint_Reset_WhenResetNewNodes_ReturnRemovedNodes() listNewEndpointSettings.Add(new EndpointSettings(6, "n6.ydb.tech", "VLA")); listNewEndpointSettings.Add(new EndpointSettings(7, "n7.ydb.tech", "MAN")); - var removed = _endpointPool.Reset(listNewEndpointSettings.ToImmutableArray()); + var removed = _endpointPool.Reset([..listNewEndpointSettings]); Assert.Equal(2, removed.Length); Assert.Equal("n1.ydb.tech", removed[0]); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/RetryPolicy/YdbRetryPolicyTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/RetryPolicy/YdbRetryPolicyTests.cs new file mode 100644 index 00000000..3a38e696 --- /dev/null +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/RetryPolicy/YdbRetryPolicyTests.cs @@ -0,0 +1,151 @@ +using Moq; +using Xunit; +using Ydb.Sdk.Ado.Internal; +using Ydb.Sdk.Ado.RetryPolicy; + +namespace Ydb.Sdk.Ado.Tests.RetryPolicy; + +public class YdbRetryPolicyTests +{ + [Theory] + [InlineData(StatusCode.BadSession)] + [InlineData(StatusCode.SessionBusy)] + public void GetNextDelay_WhenStatusIsBadSessionOrBusySession_ReturnTimeSpanZero(StatusCode statusCode) + { + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig { MaxAttempt = 2 }); + var ydbException = new YdbException(statusCode, "Mock message"); + + Assert.Equal(TimeSpan.Zero, ydbRetryPolicy.GetNextDelay(ydbException, 0)); + Assert.Equal(TimeSpan.Zero, ydbRetryPolicy.GetNextDelay(ydbException, 1)); + Assert.Null(ydbRetryPolicy.GetNextDelay(ydbException, 2)); + } + + [Theory] + [InlineData(StatusCode.ClientTransportUnavailable)] + [InlineData(StatusCode.Undetermined)] + public void GetNextDelay_WhenStatusIsIdempotenceAndDisableIdempotence_ReturnNull(StatusCode statusCode) + { + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig { MaxAttempt = 2 }); + var ydbException = new YdbException(statusCode, "Mock message"); + + Assert.Null(ydbRetryPolicy.GetNextDelay(ydbException, 0)); + Assert.Null(ydbRetryPolicy.GetNextDelay(ydbException, 1)); + } + + [Theory] + [InlineData(StatusCode.Aborted, false)] + [InlineData(StatusCode.Undetermined, true)] + public void GetNextDelay_WhenFullJitterWithFastBackoff_ReturnCalculatedBackoff(StatusCode statusCode, + bool enableRetryIdempotence) + { + var mockRandom = new Mock(MockBehavior.Strict); + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig + { + EnableRetryIdempotence = enableRetryIdempotence, + FastBackoffBaseMs = 5, + FastCapBackoffMs = 100 + }, mockRandom.Object); + var ydbException = new YdbException(statusCode, "Mock message"); + + mockRandom.Setup(random => random.Next(6)).Returns(2); + Assert.Equal(TimeSpan.FromMilliseconds(2), ydbRetryPolicy.GetNextDelay(ydbException, 0)); + + mockRandom.Setup(random => random.Next(11)).Returns(7); + Assert.Equal(TimeSpan.FromMilliseconds(7), ydbRetryPolicy.GetNextDelay(ydbException, 1)); + + mockRandom.Setup(random => random.Next(21)).Returns(14); + Assert.Equal(TimeSpan.FromMilliseconds(14), ydbRetryPolicy.GetNextDelay(ydbException, 2)); + + mockRandom.Setup(random => random.Next(41)).Returns(23); + Assert.Equal(TimeSpan.FromMilliseconds(23), ydbRetryPolicy.GetNextDelay(ydbException, 3)); + + mockRandom.Setup(random => random.Next(81)).Returns(53); + Assert.Equal(TimeSpan.FromMilliseconds(53), ydbRetryPolicy.GetNextDelay(ydbException, 4)); + + mockRandom.Setup(random => random.Next(101)).Returns(89); + Assert.Equal(TimeSpan.FromMilliseconds(89), ydbRetryPolicy.GetNextDelay(ydbException, 5)); + } + + [Theory] + [InlineData(StatusCode.Unavailable, false)] + [InlineData(StatusCode.ClientTransportUnknown, true)] + [InlineData(StatusCode.ClientTransportUnavailable, true)] + public void GetNextDelay_WhenEqualJitterWithFastBackoff_ReturnCalculatedBackoff(StatusCode statusCode, + bool enableRetryIdempotence) + { + var mockRandom = new Mock(MockBehavior.Strict); + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig + { + EnableRetryIdempotence = enableRetryIdempotence, + FastBackoffBaseMs = 5, + FastCapBackoffMs = 50 + }, mockRandom.Object); + var ydbException = new YdbException(statusCode, "Mock message"); + + mockRandom.Setup(random => random.Next(3)).Returns(1); + Assert.Equal(TimeSpan.FromMilliseconds(4), ydbRetryPolicy.GetNextDelay(ydbException, 0)); + + mockRandom.Setup(random => random.Next(6)).Returns(5); + Assert.Equal(TimeSpan.FromMilliseconds(10), ydbRetryPolicy.GetNextDelay(ydbException, 1)); + + mockRandom.Setup(random => random.Next(11)).Returns(8); + Assert.Equal(TimeSpan.FromMilliseconds(18), ydbRetryPolicy.GetNextDelay(ydbException, 2)); + + mockRandom.Setup(random => random.Next(21)).Returns(15); + Assert.Equal(TimeSpan.FromMilliseconds(35), ydbRetryPolicy.GetNextDelay(ydbException, 3)); + + mockRandom.Setup(random => random.Next(26)).Returns(11); + Assert.Equal(TimeSpan.FromMilliseconds(36), ydbRetryPolicy.GetNextDelay(ydbException, 4)); + } + + [Theory] + [InlineData(StatusCode.Overloaded, false)] + [InlineData(StatusCode.ClientTransportResourceExhausted, false)] + public void GetNextDelay_WhenEqualJitterWithSlowBackoff_ReturnCalculatedBackoff(StatusCode statusCode, + bool enableRetryIdempotence) + { + var mockRandom = new Mock(MockBehavior.Strict); + var ydbRetryPolicy = new YdbRetryPolicy(new YdbRetryPolicyConfig + { + EnableRetryIdempotence = enableRetryIdempotence, + SlowBackoffBaseMs = 100, + SlowCapBackoffMs = 1000 + }, mockRandom.Object); + var ydbException = new YdbException(statusCode, "Mock message"); + + mockRandom.Setup(random => random.Next(51)).Returns(27); + Assert.Equal(TimeSpan.FromMilliseconds(77), ydbRetryPolicy.GetNextDelay(ydbException, 0)); + + mockRandom.Setup(random => random.Next(101)).Returns(5); + Assert.Equal(TimeSpan.FromMilliseconds(105), ydbRetryPolicy.GetNextDelay(ydbException, 1)); + + mockRandom.Setup(random => random.Next(201)).Returns(123); + Assert.Equal(TimeSpan.FromMilliseconds(323), ydbRetryPolicy.GetNextDelay(ydbException, 2)); + + mockRandom.Setup(random => random.Next(401)).Returns(301); + Assert.Equal(TimeSpan.FromMilliseconds(701), ydbRetryPolicy.GetNextDelay(ydbException, 3)); + + mockRandom.Setup(random => random.Next(501)).Returns(257); + Assert.Equal(TimeSpan.FromMilliseconds(757), ydbRetryPolicy.GetNextDelay(ydbException, 4)); + } + + [Theory] + [InlineData(StatusCode.SchemeError)] + [InlineData(StatusCode.Unspecified)] + [InlineData(StatusCode.BadRequest)] + [InlineData(StatusCode.Unauthorized)] + [InlineData(StatusCode.InternalError)] + [InlineData(StatusCode.GenericError)] + [InlineData(StatusCode.Timeout)] + [InlineData(StatusCode.PreconditionFailed)] + [InlineData(StatusCode.AlreadyExists)] + [InlineData(StatusCode.NotFound)] + [InlineData(StatusCode.Cancelled)] + [InlineData(StatusCode.Unsupported)] + [InlineData(StatusCode.Success)] + [InlineData(StatusCode.ClientTransportTimeout)] + [InlineData(StatusCode.ClientTransportUnimplemented)] + public void GetNextDelay_WhenStatusCodeIsNotRetriable_ReturnNull(StatusCode statusCode) => + Assert.Null(new YdbRetryPolicy(new YdbRetryPolicyConfig { EnableRetryIdempotence = true }) + .GetNextDelay(new YdbException(statusCode, "Mock message"), 0)); +} From c44750bda2435ee718d564a3ce23dedb1c540d23 Mon Sep 17 00:00:00 2001 From: Kirill Kurdyukov Date: Fri, 29 Aug 2025 10:35:56 +0700 Subject: [PATCH 08/48] feat: added support new datetime types (#517) --- .github/workflows/tests.yml | 2 +- src/Ydb.Sdk/CHANGELOG.md | 2 + src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs | 2 - .../src/Ado/Internal/TimeSpanConsts.cs | 6 + .../YdbTypedValueExtensions.cs | 27 +- .../src/Ado/Internal/YdbValueExtensions.cs | 101 ++++++ src/Ydb.Sdk/src/Ado/ThrowHelper.cs | 15 - src/Ydb.Sdk/src/Ado/YdbCommand.cs | 1 - src/Ydb.Sdk/src/Ado/YdbDataReader.cs | 340 ++++++++++-------- src/Ydb.Sdk/src/Ado/YdbParameter.cs | 29 +- src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs | 10 +- src/Ydb.Sdk/src/Value/ResultSet.cs | 4 +- src/Ydb.Sdk/src/Value/YdbValueParser.cs | 7 +- .../test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs | 213 +++++------ .../Ydb.Sdk.Ado.Tests/YdbParameterTests.cs | 65 ++-- 15 files changed, 469 insertions(+), 355 deletions(-) create mode 100644 src/Ydb.Sdk/src/Ado/Internal/TimeSpanConsts.cs rename src/Ydb.Sdk/src/Ado/{YdbType => Internal}/YdbTypedValueExtensions.cs (83%) create mode 100644 src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs delete mode 100644 src/Ydb.Sdk/src/Ado/ThrowHelper.cs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e67486e2..c7b01fa0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: ports: [ "2135:2135", "2136:2136", "8765:8765" ] env: YDB_LOCAL_SURVIVE_RESTART: true - YDB_FEATURE_FLAGS: enable_parameterized_decimal + YDB_FEATURE_FLAGS: enable_parameterized_decimal,enable_table_datetime64 options: '--name ydb-local -h localhost' steps: diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index b44c4670..be5467e2 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,5 @@ +- Feat ADO.NET: Deleted support for `DateTimeOffset` was a mistake. +- Feat ADO.NET: Added support for `Date32`, `Datetime64`, `Timestamp64` and `Interval64` types in YDB. - Feat: Implement `YdbRetryPolicy` with AWS-inspired Exponential Backoff and Jitter. - Dev: LogLevel `Warning` -> `Debug` on DeleteSession has been `RpcException`. diff --git a/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs b/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs index 0a525d20..6af6fd05 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/SqlParam.cs @@ -1,5 +1,3 @@ -using Ydb.Sdk.Ado.YdbType; - namespace Ydb.Sdk.Ado.Internal; internal interface ISqlParam diff --git a/src/Ydb.Sdk/src/Ado/Internal/TimeSpanConsts.cs b/src/Ydb.Sdk/src/Ado/Internal/TimeSpanConsts.cs new file mode 100644 index 00000000..82f508a8 --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/Internal/TimeSpanConsts.cs @@ -0,0 +1,6 @@ +namespace Ydb.Sdk.Ado.Internal; + +internal static class TimeSpanUtils +{ + internal const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; +} diff --git a/src/Ydb.Sdk/src/Ado/YdbType/YdbTypedValueExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs similarity index 83% rename from src/Ydb.Sdk/src/Ado/YdbType/YdbTypedValueExtensions.cs rename to src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs index 3ec71829..31223be9 100644 --- a/src/Ydb.Sdk/src/Ado/YdbType/YdbTypedValueExtensions.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs @@ -1,7 +1,7 @@ using Google.Protobuf; using Google.Protobuf.WellKnownTypes; -namespace Ydb.Sdk.Ado.YdbType; +namespace Ydb.Sdk.Ado.Internal; internal static class YdbTypedValueExtensions { @@ -123,22 +123,39 @@ internal static TypedValue Uuid(this Guid value) internal static TypedValue Date(this DateTime value) => MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Date, new Ydb.Value { Uint32Value = (uint)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerDay) }); + internal static TypedValue Date32(this DateTime value) => MakePrimitiveTypedValue(Type.Types.PrimitiveTypeId.Date32, + new Ydb.Value { Int32Value = (int)(value.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerDay) }); + internal static TypedValue Datetime(this DateTime dateTimeValue) => MakePrimitiveTypedValue( - Type.Types.PrimitiveTypeId.Datetime, - new Ydb.Value + Type.Types.PrimitiveTypeId.Datetime, new Ydb.Value { Uint32Value = (uint)(dateTimeValue.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond) } ); + internal static TypedValue Datetime64(this DateTime dateTimeValue) => MakePrimitiveTypedValue( + Type.Types.PrimitiveTypeId.Datetime64, + new Ydb.Value { Int64Value = dateTimeValue.Subtract(DateTime.UnixEpoch).Ticks / TimeSpan.TicksPerSecond } + ); + internal static TypedValue Timestamp(this DateTime dateTimeValue) => MakePrimitiveTypedValue( Type.Types.PrimitiveTypeId.Timestamp, new Ydb.Value { - Uint64Value = (ulong)(dateTimeValue.Ticks - DateTime.UnixEpoch.Ticks) * Duration.NanosecondsPerTick / 1000 + Uint64Value = (ulong)(dateTimeValue.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond } ); + internal static TypedValue Timestamp64(this DateTime dateTimeValue) => MakePrimitiveTypedValue( + Type.Types.PrimitiveTypeId.Timestamp64, new Ydb.Value + { Int64Value = (dateTimeValue.Ticks - DateTime.UnixEpoch.Ticks) / TimeSpanUtils.TicksPerMicrosecond } + ); + internal static TypedValue Interval(this TimeSpan timeSpanValue) => MakePrimitiveTypedValue( Type.Types.PrimitiveTypeId.Interval, - new Ydb.Value { Int64Value = timeSpanValue.Ticks * Duration.NanosecondsPerTick / 1000 } + new Ydb.Value { Int64Value = timeSpanValue.Ticks / TimeSpanUtils.TicksPerMicrosecond } + ); + + internal static TypedValue Interval64(this TimeSpan timeSpanValue) => MakePrimitiveTypedValue( + Type.Types.PrimitiveTypeId.Interval64, + new Ydb.Value { Int64Value = timeSpanValue.Ticks / TimeSpanUtils.TicksPerMicrosecond } ); internal static TypedValue List(this IReadOnlyList values) diff --git a/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs new file mode 100644 index 00000000..c88fd149 --- /dev/null +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs @@ -0,0 +1,101 @@ +namespace Ydb.Sdk.Ado.Internal; + +internal static class YdbValueExtensions +{ + private static readonly DateTime UnixEpoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified); + + internal static bool IsNull(this Ydb.Value value) => value.ValueCase == Ydb.Value.ValueOneofCase.NullFlagValue; + + internal static bool GetBool(this Ydb.Value value) => value.BoolValue; + + internal static sbyte GetInt8(this Ydb.Value value) => (sbyte)value.Int32Value; + + internal static byte GetUint8(this Ydb.Value value) => (byte)value.Uint32Value; + + internal static short GetInt16(this Ydb.Value value) => (short)value.Int32Value; + + internal static ushort GetUint16(this Ydb.Value value) => (ushort)value.Uint32Value; + + internal static int GetInt32(this Ydb.Value value) => value.Int32Value; + + internal static uint GetUint32(this Ydb.Value value) => value.Uint32Value; + + internal static long GetInt64(this Ydb.Value value) => value.Int64Value; + + internal static ulong GetUint64(this Ydb.Value value) => value.Uint64Value; + + internal static float GetFloat(this Ydb.Value value) => value.FloatValue; + + internal static double GetDouble(this Ydb.Value value) => value.DoubleValue; + + internal static DateTime GetDate(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerDay); + + internal static DateTime GetDate32(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Int32Value * TimeSpan.TicksPerDay); + + internal static DateTime GetDatetime(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Uint32Value * TimeSpan.TicksPerSecond); + + internal static DateTime GetDatetime64(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Int64Value * TimeSpan.TicksPerSecond); + + internal static DateTime GetTimestamp(this Ydb.Value value) => + UnixEpoch.AddTicks((long)(value.Uint64Value * TimeSpanUtils.TicksPerMicrosecond)); + + internal static DateTime GetTimestamp64(this Ydb.Value value) => + UnixEpoch.AddTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + + internal static TimeSpan GetInterval(this Ydb.Value value) => + TimeSpan.FromTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + + internal static TimeSpan GetInterval64(this Ydb.Value value) => + TimeSpan.FromTicks(value.Int64Value * TimeSpanUtils.TicksPerMicrosecond); + + internal static byte[] GetBytes(this Ydb.Value value) => value.BytesValue.ToByteArray(); + + internal static string GetText(this Ydb.Value value) => value.TextValue; + + internal static string GetJson(this Ydb.Value value) => value.TextValue; + + internal static string GetJsonDocument(this Ydb.Value value) => value.TextValue; + + internal static Guid GetUuid(this Ydb.Value value) + { + var high = value.High128; + var low = value.Low128; + + var lowBytes = BitConverter.GetBytes(low); + var highBytes = BitConverter.GetBytes(high); + + var guidBytes = new byte[16]; + Array.Copy(lowBytes, 0, guidBytes, 0, 8); + Array.Copy(highBytes, 0, guidBytes, 8, 8); + + return new Guid(guidBytes); + } + + internal static decimal GetDecimal(this Ydb.Value value, uint scale) + { + var lo = value.Low128; + var hi = value.High128; + var isNegative = (hi & 0x8000_0000_0000_0000UL) != 0; + unchecked + { + if (isNegative) + { + if (lo == 0) + hi--; + + lo--; + lo = ~lo; + hi = ~hi; + } + } + + if (hi >> 32 != 0) + throw new OverflowException("Value does not fit into decimal"); + + return new decimal((int)lo, (int)(lo >> 32), (int)hi, isNegative, (byte)scale); + } +} diff --git a/src/Ydb.Sdk/src/Ado/ThrowHelper.cs b/src/Ydb.Sdk/src/Ado/ThrowHelper.cs deleted file mode 100644 index 4dd739d7..00000000 --- a/src/Ydb.Sdk/src/Ado/ThrowHelper.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Ydb.Sdk.Value; - -namespace Ydb.Sdk.Ado; - -internal static class ThrowHelper -{ - internal static T ThrowInvalidCast(YdbValue ydbValue) => - throw new InvalidCastException($"Field YDB type {ydbValue.TypeId} can't be cast to {typeof(T)} type."); - - internal static void ThrowIndexOutOfRangeException(int columnCount) => - throw new IndexOutOfRangeException("Ordinal must be between 0 and " + (columnCount - 1)); - - internal static void ThrowInvalidCastException(string expectedType, string actualType) => - throw new InvalidCastException($"Invalid type of YDB value, expected: {expectedType}, actual: {actualType}."); -} diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 7be84fa7..ac6d8c2f 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using Ydb.Sdk.Ado.Internal; -using Ydb.Sdk.Ado.YdbType; namespace Ydb.Sdk.Ado; diff --git a/src/Ydb.Sdk/src/Ado/YdbDataReader.cs b/src/Ydb.Sdk/src/Ado/YdbDataReader.cs index 5bcd0e8f..fa4a7141 100644 --- a/src/Ydb.Sdk/src/Ado/YdbDataReader.cs +++ b/src/Ydb.Sdk/src/Ado/YdbDataReader.cs @@ -3,10 +3,10 @@ using Ydb.Issue; using Ydb.Query; using Ydb.Sdk.Ado.Internal; -using Ydb.Sdk.Value; namespace Ydb.Sdk.Ado; +// ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault public sealed class YdbDataReader : DbDataReader, IAsyncEnumerable { private readonly IServerStream _stream; @@ -16,7 +16,7 @@ public sealed class YdbDataReader : DbDataReader, IAsyncEnumerable this switch + private ResultSet CurrentResultSet => this switch { { ReaderState: State.ReadResultSet, _currentRowIndex: >= 0 } => _currentResultSet!, { ReaderState: State.Close } => throw new InvalidOperationException("The reader is closed"), _ => throw new InvalidOperationException("No row is available") }; - private Value.ResultSet.Row CurrentRow => CurrentResultSet.Rows[_currentRowIndex]; + private IReadOnlyList CurrentRow => CurrentResultSet.Rows[_currentRowIndex].Items; private int RowsCount => ReaderMetadata.RowsCount; private enum State @@ -86,14 +86,15 @@ private async Task Init(CancellationToken cancellationToken) ReaderState = State.ReadResultSet; } - public override bool GetBoolean(int ordinal) => GetFieldYdbValue(ordinal).GetBool(); + public override bool GetBoolean(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Bool, ordinal).GetBool(); - public override byte GetByte(int ordinal) => GetFieldYdbValue(ordinal).GetUint8(); + public override byte GetByte(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Uint8, ordinal).GetUint8(); - public sbyte GetSByte(int ordinal) => GetFieldYdbValue(ordinal).GetInt8(); + public sbyte GetSByte(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Int8, ordinal).GetInt8(); - // ReSharper disable once MemberCanBePrivate.Global - public byte[] GetBytes(int ordinal) => GetFieldYdbValue(ordinal).GetString(); + public byte[] GetBytes(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.String, ordinal).GetBytes(); public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) { @@ -121,6 +122,7 @@ public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int public override char GetChar(int ordinal) { var str = GetString(ordinal); + return str.Length == 0 ? throw new InvalidCastException("Could not read char - string was empty") : str[0]; } @@ -174,30 +176,50 @@ private static void CheckOffsets(long dataOffset, T[]? buffer, int bufferOffs public override DateTime GetDateTime(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Timestamp => ydbValue.GetTimestamp(), - YdbTypeId.Datetime => ydbValue.GetDatetime(), - YdbTypeId.Date => ydbValue.GetDate(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Timestamp => CurrentRow[ordinal].GetTimestamp(), + Type.Types.PrimitiveTypeId.Datetime => CurrentRow[ordinal].GetDatetime(), + Type.Types.PrimitiveTypeId.Date => CurrentRow[ordinal].GetDate(), + Type.Types.PrimitiveTypeId.Timestamp64 => CurrentRow[ordinal].GetTimestamp64(), + Type.Types.PrimitiveTypeId.Datetime64 => CurrentRow[ordinal].GetDatetime64(), + Type.Types.PrimitiveTypeId.Date32 => CurrentRow[ordinal].GetDate32(), + _ => throw InvalidCastException(ordinal) }; } - public TimeSpan GetInterval(int ordinal) => GetFieldYdbValue(ordinal).GetInterval(); + public TimeSpan GetInterval(int ordinal) + { + var type = UnwrapColumnType(ordinal); - public override decimal GetDecimal(int ordinal) => GetFieldYdbValue(ordinal).GetDecimal(); + return type.TypeId switch + { + Type.Types.PrimitiveTypeId.Interval => CurrentRow[ordinal].GetInterval(), + Type.Types.PrimitiveTypeId.Interval64 => CurrentRow[ordinal].GetInterval64(), + _ => throw InvalidCastException(ordinal) + }; + } + + public override decimal GetDecimal(int ordinal) + { + var type = UnwrapColumnType(ordinal); + + return type.TypeCase == Type.TypeOneofCase.DecimalType + ? CurrentRow[ordinal].GetDecimal((byte)type.DecimalType.Scale) + : throw InvalidCastException(Type.TypeOneofCase.DecimalType, ordinal); + } public override double GetDouble(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Float => ydbValue.GetFloat(), - YdbTypeId.Double => ydbValue.GetDouble(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Double => CurrentRow[ordinal].GetDouble(), + Type.Types.PrimitiveTypeId.Float => CurrentRow[ordinal].GetFloat(), + _ => throw InvalidCastException(ordinal) }; } @@ -230,117 +252,126 @@ public override System.Type GetFieldType(int ordinal) type = type.OptionalType.Item; } - var systemType = YdbValue.GetYdbTypeId(type) switch - { - YdbTypeId.Timestamp or YdbTypeId.Datetime or YdbTypeId.Date => typeof(DateTime), - YdbTypeId.Bool => typeof(bool), - YdbTypeId.Int8 => typeof(sbyte), - YdbTypeId.Uint8 => typeof(byte), - YdbTypeId.Int16 => typeof(short), - YdbTypeId.Uint16 => typeof(ushort), - YdbTypeId.Int32 => typeof(int), - YdbTypeId.Uint32 => typeof(uint), - YdbTypeId.Int64 => typeof(long), - YdbTypeId.Uint64 => typeof(ulong), - YdbTypeId.Float => typeof(float), - YdbTypeId.Double => typeof(double), - YdbTypeId.Interval => typeof(TimeSpan), - YdbTypeId.Utf8 or YdbTypeId.JsonDocument or YdbTypeId.Json or YdbTypeId.Yson => - typeof(string), - YdbTypeId.String => typeof(byte[]), - YdbTypeId.DecimalType => typeof(decimal), - YdbTypeId.Uuid => typeof(Guid), + if (type.TypeCase == Type.TypeOneofCase.DecimalType) + { + return typeof(decimal); + } + + return type.TypeId switch + { + Type.Types.PrimitiveTypeId.Date + or Type.Types.PrimitiveTypeId.Date32 + or Type.Types.PrimitiveTypeId.Datetime + or Type.Types.PrimitiveTypeId.Datetime64 + or Type.Types.PrimitiveTypeId.Timestamp + or Type.Types.PrimitiveTypeId.Timestamp64 => typeof(DateTime), + Type.Types.PrimitiveTypeId.Bool => typeof(bool), + Type.Types.PrimitiveTypeId.Int8 => typeof(sbyte), + Type.Types.PrimitiveTypeId.Uint8 => typeof(byte), + Type.Types.PrimitiveTypeId.Int16 => typeof(short), + Type.Types.PrimitiveTypeId.Uint16 => typeof(ushort), + Type.Types.PrimitiveTypeId.Int32 => typeof(int), + Type.Types.PrimitiveTypeId.Uint32 => typeof(uint), + Type.Types.PrimitiveTypeId.Int64 => typeof(long), + Type.Types.PrimitiveTypeId.Uint64 => typeof(ulong), + Type.Types.PrimitiveTypeId.Float => typeof(float), + Type.Types.PrimitiveTypeId.Double => typeof(double), + Type.Types.PrimitiveTypeId.Interval => typeof(TimeSpan), + Type.Types.PrimitiveTypeId.Utf8 + or Type.Types.PrimitiveTypeId.JsonDocument + or Type.Types.PrimitiveTypeId.Json => typeof(string), + Type.Types.PrimitiveTypeId.String => typeof(byte[]), + Type.Types.PrimitiveTypeId.Uuid => typeof(Guid), _ => throw new YdbException($"Unsupported ydb type {type}") }; - - return systemType; } - public override float GetFloat(int ordinal) => GetFieldYdbValue(ordinal).GetFloat(); + public override float GetFloat(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Float, ordinal).GetFloat(); - public override Guid GetGuid(int ordinal) => GetFieldYdbValue(ordinal).GetUuid(); + public override Guid GetGuid(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Uuid, ordinal).GetUuid(); public override short GetInt16(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Int8 => ydbValue.GetInt8(), - YdbTypeId.Int16 => ydbValue.GetInt16(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].GetInt16(), + Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].GetInt8(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public ushort GetUint16(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public override int GetInt32(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Int32 => ydbValue.GetInt32(), - YdbTypeId.Int8 => ydbValue.GetInt8(), - YdbTypeId.Int16 => ydbValue.GetInt16(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Int32 => CurrentRow[ordinal].GetInt32(), + Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].GetInt16(), + Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].GetInt8(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public uint GetUint32(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - YdbTypeId.Uint32 => ydbValue.GetUint32(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].GetUint32(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public override long GetInt64(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Int64 => ydbValue.GetInt64(), - YdbTypeId.Int32 => ydbValue.GetInt32(), - YdbTypeId.Int8 => ydbValue.GetInt8(), - YdbTypeId.Int16 => ydbValue.GetInt16(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - YdbTypeId.Uint32 => ydbValue.GetUint32(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Int64 => CurrentRow[ordinal].GetInt64(), + Type.Types.PrimitiveTypeId.Int32 => CurrentRow[ordinal].GetInt32(), + Type.Types.PrimitiveTypeId.Int16 => CurrentRow[ordinal].GetInt16(), + Type.Types.PrimitiveTypeId.Int8 => CurrentRow[ordinal].GetInt8(), + Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].GetUint32(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } public ulong GetUint64(int ordinal) { - var ydbValue = GetFieldYdbValue(ordinal); + var type = UnwrapColumnType(ordinal); - return ydbValue.TypeId switch + return type.TypeId switch { - YdbTypeId.Uint64 => ydbValue.GetUint64(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - YdbTypeId.Uint32 => ydbValue.GetUint32(), - _ => ThrowHelper.ThrowInvalidCast(ydbValue) + Type.Types.PrimitiveTypeId.Uint64 => CurrentRow[ordinal].GetUint64(), + Type.Types.PrimitiveTypeId.Uint32 => CurrentRow[ordinal].GetUint32(), + Type.Types.PrimitiveTypeId.Uint16 => CurrentRow[ordinal].GetUint16(), + Type.Types.PrimitiveTypeId.Uint8 => CurrentRow[ordinal].GetUint8(), + _ => throw InvalidCastException(ordinal) }; } @@ -356,58 +387,62 @@ public override int GetOrdinal(string name) throw new IndexOutOfRangeException($"Field not found in row: {name}"); } - public override string GetString(int ordinal) => GetFieldYdbValue(ordinal).GetUtf8(); + public override string GetString(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.Utf8, ordinal).GetText(); public override TextReader GetTextReader(int ordinal) => new StringReader(GetString(ordinal)); - public string GetJson(int ordinal) => GetFieldYdbValue(ordinal).GetJson(); + public string GetJson(int ordinal) => GetPrimitiveValue(Type.Types.PrimitiveTypeId.Json, ordinal).GetJson(); - public string GetJsonDocument(int ordinal) => GetFieldYdbValue(ordinal).GetJsonDocument(); + public string GetJsonDocument(int ordinal) => + GetPrimitiveValue(Type.Types.PrimitiveTypeId.JsonDocument, ordinal).GetJsonDocument(); public override object GetValue(int ordinal) { + var type = GetColumnType(ordinal); var ydbValue = CurrentRow[ordinal]; - // ReSharper disable once ConvertIfStatementToSwitchStatement - if (ydbValue.TypeId == YdbTypeId.Null) + if (ydbValue.IsNull()) { return DBNull.Value; } - // ReSharper disable once InvertIf - if (ydbValue.TypeId == YdbTypeId.OptionalType) + if (type.TypeCase == Type.TypeOneofCase.OptionalType) { - if (ydbValue.GetOptional() == null) - { - return DBNull.Value; - } + type = type.OptionalType.Item; + } - ydbValue = ydbValue.GetOptional()!; - } - - return ydbValue.TypeId switch - { - YdbTypeId.Timestamp or YdbTypeId.Datetime or YdbTypeId.Date => GetDateTime(ordinal), - YdbTypeId.Bool => ydbValue.GetBool(), - YdbTypeId.Int8 => ydbValue.GetInt8(), - YdbTypeId.Uint8 => ydbValue.GetUint8(), - YdbTypeId.Int16 => ydbValue.GetInt16(), - YdbTypeId.Uint16 => ydbValue.GetUint16(), - YdbTypeId.Int32 => ydbValue.GetInt32(), - YdbTypeId.Uint32 => ydbValue.GetUint32(), - YdbTypeId.Int64 => ydbValue.GetInt64(), - YdbTypeId.Uint64 => ydbValue.GetUint64(), - YdbTypeId.Float => ydbValue.GetFloat(), - YdbTypeId.Double => ydbValue.GetDouble(), - YdbTypeId.Interval => ydbValue.GetInterval(), - YdbTypeId.Utf8 => ydbValue.GetUtf8(), - YdbTypeId.Json => ydbValue.GetJson(), - YdbTypeId.JsonDocument => ydbValue.GetJsonDocument(), - YdbTypeId.Yson => ydbValue.GetYson(), - YdbTypeId.String => ydbValue.GetString(), - YdbTypeId.DecimalType => ydbValue.GetDecimal(), - YdbTypeId.Uuid => ydbValue.GetUuid(), - _ => throw new YdbException($"Unsupported ydb type {ydbValue.TypeId}") + if (type.TypeCase == Type.TypeOneofCase.DecimalType) + { + return ydbValue.GetDecimal(type.DecimalType.Scale); + } + + return type.TypeId switch + { + Type.Types.PrimitiveTypeId.Date => ydbValue.GetDate(), + Type.Types.PrimitiveTypeId.Date32 => ydbValue.GetDate32(), + Type.Types.PrimitiveTypeId.Datetime => ydbValue.GetDatetime(), + Type.Types.PrimitiveTypeId.Datetime64 => ydbValue.GetDatetime64(), + Type.Types.PrimitiveTypeId.Timestamp => ydbValue.GetTimestamp(), + Type.Types.PrimitiveTypeId.Timestamp64 => ydbValue.GetTimestamp64(), + Type.Types.PrimitiveTypeId.Bool => ydbValue.GetBool(), + Type.Types.PrimitiveTypeId.Int8 => ydbValue.GetInt8(), + Type.Types.PrimitiveTypeId.Uint8 => ydbValue.GetUint8(), + Type.Types.PrimitiveTypeId.Int16 => ydbValue.GetInt16(), + Type.Types.PrimitiveTypeId.Uint16 => ydbValue.GetUint16(), + Type.Types.PrimitiveTypeId.Int32 => ydbValue.GetInt32(), + Type.Types.PrimitiveTypeId.Uint32 => ydbValue.GetUint32(), + Type.Types.PrimitiveTypeId.Int64 => ydbValue.GetInt64(), + Type.Types.PrimitiveTypeId.Uint64 => ydbValue.GetUint64(), + Type.Types.PrimitiveTypeId.Float => ydbValue.GetFloat(), + Type.Types.PrimitiveTypeId.Double => ydbValue.GetDouble(), + Type.Types.PrimitiveTypeId.Interval => ydbValue.GetInterval(), + Type.Types.PrimitiveTypeId.Utf8 => ydbValue.GetText(), + Type.Types.PrimitiveTypeId.Json => ydbValue.GetJson(), + Type.Types.PrimitiveTypeId.JsonDocument => ydbValue.GetJsonDocument(), + Type.Types.PrimitiveTypeId.String => ydbValue.GetBytes(), + Type.Types.PrimitiveTypeId.Uuid => ydbValue.GetUuid(), + _ => throw new YdbException($"Unsupported ydb type {GetColumnType(ordinal)}") }; } @@ -416,7 +451,7 @@ public override int GetValues(object[] values) ArgumentNullException.ThrowIfNull(values); if (FieldCount == 0) { - throw new InvalidOperationException(" No resultset is currently being traversed"); + throw new InvalidOperationException("No resultset is currently being traversed"); } var count = Math.Min(FieldCount, values.Length); @@ -425,9 +460,7 @@ public override int GetValues(object[] values) return count; } - public override bool IsDBNull(int ordinal) => - CurrentRow[ordinal].TypeId == YdbTypeId.Null || - (CurrentRow[ordinal].TypeId == YdbTypeId.OptionalType && CurrentRow[ordinal].GetOptional() == null); + public override bool IsDBNull(int ordinal) => CurrentRow[ordinal].IsNull(); public override int FieldCount => ReaderMetadata.FieldCount; public override object this[int ordinal] => GetValue(ordinal); @@ -536,13 +569,24 @@ public override async Task CloseAsync() public override void Close() => CloseAsync().GetAwaiter().GetResult(); - private YdbValue GetFieldYdbValue(int ordinal) + private Type UnwrapColumnType(int ordinal) { + var type = GetColumnType(ordinal); + + if (CurrentRow[ordinal].IsNull()) + throw new InvalidCastException("Field is null."); + + return type.TypeCase == Type.TypeOneofCase.OptionalType ? type.OptionalType.Item : type; + } + + private Type GetColumnType(int ordinal) => ReaderMetadata.GetColumn(ordinal).Type; + + private Ydb.Value GetPrimitiveValue(Type.Types.PrimitiveTypeId primitiveTypeId, int ordinal) + { + var type = UnwrapColumnType(ordinal); var ydbValue = CurrentRow[ordinal]; - return ydbValue.TypeId == YdbTypeId.OptionalType - ? ydbValue.GetOptional() ?? throw new InvalidCastException("Field is null.") - : ydbValue; + return type.TypeId == primitiveTypeId ? ydbValue : throw InvalidCastException(primitiveTypeId, ordinal); } private async ValueTask NextExecPart(CancellationToken cancellationToken) @@ -570,7 +614,7 @@ private async ValueTask NextExecPart(CancellationToken cancellationToken) throw YdbException.FromServer(part.Status, _issueMessagesInStream); } - _currentResultSet = part.ResultSet?.FromProto(); + _currentResultSet = part.ResultSet; ReaderMetadata = _currentResultSet != null ? new Metadata(_currentResultSet) : EmptyMetadata.Instance; if (_ydbTransaction != null && part.TxMeta != null) @@ -631,7 +675,7 @@ private EmptyMetadata() public int FieldCount => 0; public int RowsCount => 0; - public Value.ResultSet.Column GetColumn(int ordinal) => + public Column GetColumn(int ordinal) => throw new InvalidOperationException("No resultset is currently being traversed"); } @@ -649,34 +693,44 @@ private CloseMetadata() public int FieldCount => throw new InvalidOperationException("The reader is closed"); public int RowsCount => 0; - public Value.ResultSet.Column GetColumn(int ordinal) => - throw new InvalidOperationException("The reader is closed"); + public Column GetColumn(int ordinal) => throw new InvalidOperationException("The reader is closed"); } private class Metadata : IMetadata { - private IReadOnlyList Columns { get; } + private IReadOnlyList Columns { get; } public IReadOnlyDictionary ColumnNameToOrdinal { get; } public int FieldCount { get; } public int RowsCount { get; } - public Metadata(Value.ResultSet resultSet) + public Metadata(ResultSet resultSet) { - ColumnNameToOrdinal = resultSet.ColumnNameToOrdinal; Columns = resultSet.Columns; + ColumnNameToOrdinal = ColumnNameToOrdinal = Columns + .Select((c, idx) => (c.Name, Index: idx)) + .ToDictionary(t => t.Name, t => t.Index); RowsCount = resultSet.Rows.Count; FieldCount = resultSet.Columns.Count; } - public Value.ResultSet.Column GetColumn(int ordinal) + public Column GetColumn(int ordinal) { if (ordinal < 0 || ordinal >= FieldCount) { - ThrowHelper.ThrowIndexOutOfRangeException(FieldCount); + throw new IndexOutOfRangeException("Ordinal must be between 0 and " + (FieldCount - 1)); } return Columns[ordinal]; } } + + private InvalidCastException InvalidCastException(int ordinal) => + new($"Field YDB type {GetColumnType(ordinal)} can't be cast to {typeof(T)} type."); + + private InvalidCastException InvalidCastException(Type.Types.PrimitiveTypeId expectedType, int ordinal) => + new($"Invalid type of YDB value, expected primitive typeId: {expectedType}, actual: {GetColumnType(ordinal)}."); + + private InvalidCastException InvalidCastException(Type.TypeOneofCase expectedType, int ordinal) + => new($"Invalid type of YDB value, expected: {expectedType}, actual: {GetColumnType(ordinal)}."); } diff --git a/src/Ydb.Sdk/src/Ado/YdbParameter.cs b/src/Ydb.Sdk/src/Ado/YdbParameter.cs index 78ffbec9..2d4ac898 100644 --- a/src/Ydb.Sdk/src/Ado/YdbParameter.cs +++ b/src/Ydb.Sdk/src/Ado/YdbParameter.cs @@ -2,14 +2,16 @@ using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; +using Ydb.Sdk.Ado.Internal; using Ydb.Sdk.Ado.YdbType; using Ydb.Sdk.Value; +using static Ydb.Sdk.Ado.Internal.YdbTypedValueExtensions; namespace Ydb.Sdk.Ado; public sealed class YdbParameter : DbParameter { - private static readonly TypedValue NullDefaultDecimal = YdbTypedValueExtensions.NullDecimal(22, 9); + private static readonly TypedValue NullDefaultDecimal = NullDecimal(22, 9); private static readonly Dictionary YdbNullByDbType = new() { @@ -32,7 +34,11 @@ public sealed class YdbParameter : DbParameter { YdbDbType.Double, Type.Types.PrimitiveTypeId.Double.Null() }, { YdbDbType.Uuid, Type.Types.PrimitiveTypeId.Uuid.Null() }, { YdbDbType.Json, Type.Types.PrimitiveTypeId.Json.Null() }, - { YdbDbType.JsonDocument, Type.Types.PrimitiveTypeId.JsonDocument.Null() } + { YdbDbType.JsonDocument, Type.Types.PrimitiveTypeId.JsonDocument.Null() }, + { YdbDbType.Date32, Type.Types.PrimitiveTypeId.Date32.Null() }, + { YdbDbType.Datetime64, Type.Types.PrimitiveTypeId.Datetime64.Null() }, + { YdbDbType.Timestamp64, Type.Types.PrimitiveTypeId.Timestamp64.Null() }, + { YdbDbType.Interval64, Type.Types.PrimitiveTypeId.Interval64.Null() } }; private string _parameterName = string.Empty; @@ -153,9 +159,13 @@ internal TypedValue TypedValue YdbDbType.JsonDocument when value is string stringValue => stringValue.JsonDocument(), YdbDbType.Uuid when value is Guid guidValue => guidValue.Uuid(), YdbDbType.Date => MakeDate(value), + YdbDbType.Date32 => MakeDate32(value), YdbDbType.DateTime when value is DateTime dateTimeValue => dateTimeValue.Datetime(), - YdbDbType.Timestamp => MakeTimestamp(value), + YdbDbType.Datetime64 when value is DateTime dateTimeValue => dateTimeValue.Datetime64(), + YdbDbType.Timestamp when value is DateTime dateTimeValue => dateTimeValue.Timestamp(), + YdbDbType.Timestamp64 when value is DateTime dateTimeValue => dateTimeValue.Timestamp64(), YdbDbType.Interval when value is TimeSpan timeSpanValue => timeSpanValue.Interval(), + YdbDbType.Interval64 when value is TimeSpan timeSpanValue => timeSpanValue.Interval64(), YdbDbType.Unspecified => Cast(value), _ => throw ValueTypeNotSupportedException }; @@ -237,10 +247,10 @@ internal TypedValue TypedValue _ => throw ValueTypeNotSupportedException }; - private TypedValue MakeTimestamp(object value) => value switch + private TypedValue MakeDate32(object value) => value switch { - DateTime dateTimeValue => dateTimeValue.Timestamp(), - DateTimeOffset dateTimeOffsetValue => dateTimeOffsetValue.UtcDateTime.Timestamp(), + DateTime dateTimeValue => dateTimeValue.Date32(), + DateOnly dateOnlyValue => dateOnlyValue.ToDateTime(TimeOnly.MinValue).Date32(), _ => throw ValueTypeNotSupportedException }; @@ -261,7 +271,6 @@ internal TypedValue TypedValue decimal decimalValue => Decimal(decimalValue), Guid guidValue => guidValue.Uuid(), DateTime dateTimeValue => dateTimeValue.Timestamp(), - DateTimeOffset dateTimeOffset => dateTimeOffset.UtcDateTime.Timestamp(), DateOnly dateOnlyValue => dateOnlyValue.ToDateTime(TimeOnly.MinValue).Date(), byte[] bytesValue => bytesValue.Bytes(), TimeSpan timeSpanValue => timeSpanValue.Interval(), @@ -271,9 +280,7 @@ internal TypedValue TypedValue }; private TypedValue Decimal(decimal value) => - Precision == 0 && Scale == 0 - ? value.Decimal(22, 9) - : value.Decimal(Precision, Scale); + Precision == 0 && Scale == 0 ? value.Decimal(22, 9) : value.Decimal(Precision, Scale); private TypedValue NullTypedValue() { @@ -286,7 +293,7 @@ private TypedValue NullTypedValue() { return Precision == 0 && Scale == 0 ? NullDefaultDecimal - : YdbTypedValueExtensions.NullDecimal(Precision, Scale); + : NullDecimal(Precision, Scale); } throw new InvalidOperationException( diff --git a/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs b/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs index b170df00..8a4306b1 100644 --- a/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs +++ b/src/Ydb.Sdk/src/Ado/YdbType/YdbDbType.cs @@ -158,7 +158,15 @@ public enum YdbDbType /// Value range: From -136 years to +136 years. Internal representation: Signed 64-bit integer. /// Can't be used in the primary key. /// - Interval + Interval, + + Date32, + + Datetime64, + + Timestamp64, + + Interval64 } internal static class YdbDbTypeExtensions diff --git a/src/Ydb.Sdk/src/Value/ResultSet.cs b/src/Ydb.Sdk/src/Value/ResultSet.cs index 108ab630..1b71c44f 100644 --- a/src/Ydb.Sdk/src/Value/ResultSet.cs +++ b/src/Ydb.Sdk/src/Value/ResultSet.cs @@ -1,6 +1,5 @@ using System.Collections; using Google.Protobuf.Collections; -using Ydb.Sdk.Ado; namespace Ydb.Sdk.Value; @@ -19,7 +18,6 @@ public class ResultSet internal ResultSet(Ydb.ResultSet resultSetProto) { Columns = resultSetProto.Columns.Select(c => new Column(c.Type, c.Name)).ToArray(); - ColumnNameToOrdinal = Columns .Select((c, idx) => (c.Name, Index: idx)) .ToDictionary(t => t.Name, t => t.Index); @@ -116,7 +114,7 @@ public YdbValue this[int columnIndex] { if (columnIndex < 0 || columnIndex >= ColumnCount) { - ThrowHelper.ThrowIndexOutOfRangeException(ColumnCount); + throw new IndexOutOfRangeException("Ordinal must be between 0 and " + (ColumnCount - 1)); } return new YdbValue(_columns[columnIndex].Type, _row.Items[columnIndex]); diff --git a/src/Ydb.Sdk/src/Value/YdbValueParser.cs b/src/Ydb.Sdk/src/Value/YdbValueParser.cs index 51ed43e9..914f7878 100644 --- a/src/Ydb.Sdk/src/Value/YdbValueParser.cs +++ b/src/Ydb.Sdk/src/Value/YdbValueParser.cs @@ -1,5 +1,4 @@ using Google.Protobuf.WellKnownTypes; -using Ydb.Sdk.Ado; namespace Ydb.Sdk.Value; @@ -258,7 +257,8 @@ private void EnsureType(Type.TypeOneofCase expectedType) { if (_protoType.TypeCase != expectedType) { - ThrowHelper.ThrowInvalidCastException(expectedType.ToString(), TypeId.ToString()); + throw new InvalidCastException( + $"Invalid type of YDB value, expected: {expectedType}, actual: {_protoType}."); } } @@ -266,7 +266,8 @@ private void EnsurePrimitiveTypeId(Type.Types.PrimitiveTypeId primitiveTypeId) { if (_protoType.TypeCase != Type.TypeOneofCase.TypeId || _protoType.TypeId != primitiveTypeId) { - ThrowHelper.ThrowInvalidCastException(primitiveTypeId.ToString(), TypeId.ToString()); + throw new InvalidCastException( + $"Invalid type of YDB value, expected: {primitiveTypeId}, actual: {_protoType}."); } } } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs index 8d8a1ce4..05ca0c1c 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs @@ -1,6 +1,5 @@ using System.Collections; using System.Data; -using System.Text; using Xunit; using Ydb.Sdk.Value; @@ -60,6 +59,30 @@ public async Task ExecuteScalarAsync_WhenSetYdbParameterThenPrepare_ReturnThisVa Assert.Equal(data.Expected == null ? DBNull.Value : data.Expected, await dbCommand.ExecuteScalarAsync()); } + [Fact] + public async Task ExecuteReaderAsync_WhenSelectNull_ThrowFieldIsNull() + { + await using var connection = await CreateOpenConnectionAsync(); + var dbCommand = connection.CreateCommand(); + dbCommand.CommandText = "SELECT NULL"; + var reader = await dbCommand.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.Equal("Field is null.", Assert.Throws(() => reader.GetFloat(0)).Message); + } + + [Fact] + public async Task ExecuteReaderAsync_WhenOptionalIsNull_ThrowFieldIsNull() + { + await using var connection = await CreateOpenConnectionAsync(); + var dbCommand = connection.CreateCommand(); + dbCommand.CommandText = "SELECT CAST(NULL AS Optional) AS Field"; + var reader = await dbCommand.ExecuteReaderAsync(); + await reader.ReadAsync(); + + Assert.Equal("Field is null.", Assert.Throws(() => reader.GetFloat(0)).Message); + } + [Theory] [ClassData(typeof(TestDataGenerator))] public async Task ExecuteScalarAsync_WhenDbTypeIsObject_ReturnThisValue(Data data) @@ -94,11 +117,9 @@ public async Task ExecuteScalarAsync_WhenNoDbTypeParameter_ReturnThisValue() (YdbValue.MakeJson(simpleJson), simpleJson), (YdbValue.MakeJsonDocument(simpleJson), simpleJson), (YdbValue.MakeInterval(TimeSpan.FromSeconds(5)), TimeSpan.FromSeconds(5)), - (YdbValue.MakeYson("{type=\"yson\"}"u8.ToArray()), "{type=\"yson\"}"u8.ToArray()), (YdbValue.MakeOptionalJson(simpleJson), simpleJson), (YdbValue.MakeOptionalJsonDocument(simpleJson), simpleJson), - (YdbValue.MakeOptionalInterval(TimeSpan.FromSeconds(5)), TimeSpan.FromSeconds(5)), - (YdbValue.MakeOptionalYson("{type=\"yson\"}"u8.ToArray()), "{type=\"yson\"}"u8.ToArray()) + (YdbValue.MakeOptionalInterval(TimeSpan.FromSeconds(5)), TimeSpan.FromSeconds(5)) }; await using var connection = await CreateOpenConnectionAsync(); @@ -185,138 +206,78 @@ public async Task ExecuteScalar_WhenSelectNoRows_ReturnNull() } - public class Data + public class Data(DbType dbType, T expected, bool isNullable = false) { - public Data(DbType dbType, T expected, Func fetchFun, bool isNullable = false) - { - DbType = dbType; - Expected = expected; - IsNullable = isNullable || expected == null; - FetchFun = fetchFun; - } - - public bool IsNullable { get; } - public DbType DbType { get; } - public T Expected { get; } - public Func FetchFun { get; } + public bool IsNullable { get; } = isNullable || expected == null; + public DbType DbType { get; } = dbType; + public T Expected { get; } = expected; } - public class TestDataGenerator : IEnumerable + private class TestDataGenerator : IEnumerable { private readonly List _data = [ - new object[] { new Data(DbType.Boolean, true, value => value.GetBool()) }, - new object[] { new Data(DbType.Boolean, false, value => value.GetBool()) }, - new object[] { new Data(DbType.Boolean, true, value => value.GetBool(), true) }, - new object[] { new Data(DbType.Boolean, false, value => value.GetBool(), true) }, - new object[] { new Data(DbType.Boolean, null, value => value.GetOptionalBool()) }, - new object[] { new Data(DbType.SByte, -1, value => value.GetInt8()) }, - new object[] { new Data(DbType.SByte, -2, value => value.GetInt8(), true) }, - new object[] { new Data(DbType.SByte, null, value => value.GetOptionalInt8()) }, - new object[] { new Data(DbType.Byte, 200, value => value.GetUint8()) }, - new object[] { new Data(DbType.Byte, 228, value => value.GetUint8(), true) }, - new object[] { new Data(DbType.Byte, null, value => value.GetOptionalUint8()) }, - new object[] { new Data(DbType.Int16, 14000, value => value.GetInt16()) }, - new object[] { new Data(DbType.Int16, 14000, value => value.GetInt16(), true) }, - new object[] { new Data(DbType.Int16, null, value => value.GetOptionalInt16()) }, - new object[] { new Data(DbType.UInt16, 40_000, value => value.GetUint16()) }, - new object[] { new Data(DbType.UInt16, 40_000, value => value.GetUint16(), true) }, - new object[] { new Data(DbType.UInt16, null, value => value.GetOptionalUint16()) }, - new object[] { new Data(DbType.Int32, -40_000, value => value.GetInt32()) }, - new object[] { new Data(DbType.Int32, -40_000, value => value.GetInt32(), true) }, - new object[] { new Data(DbType.Int32, null, value => value.GetOptionalInt32()) }, - new object[] { new Data(DbType.UInt32, 4_000_000_000, value => value.GetUint32()) }, - new object[] { new Data(DbType.UInt32, 4_000_000_000, value => value.GetUint32(), true) }, - new object[] { new Data(DbType.UInt32, null, value => value.GetOptionalUint32()) }, - new object[] { new Data(DbType.Int64, -4_000_000_000, value => value.GetInt64()) }, - new object[] { new Data(DbType.Int64, -4_000_000_000, value => value.GetInt64(), true) }, - new object[] { new Data(DbType.Int64, null, value => value.GetOptionalInt64()) }, - new object[] { new Data(DbType.UInt64, 10_000_000_000ul, value => value.GetUint64()) }, - new object[] - { new Data(DbType.UInt64, 10_000_000_000ul, value => value.GetUint64(), true) }, - - new object[] { new Data(DbType.UInt64, null, value => value.GetOptionalUint64()) }, - new object[] { new Data(DbType.Single, -1.7f, value => value.GetFloat()) }, - new object[] { new Data(DbType.Single, -1.7f, value => value.GetFloat(), true) }, - new object[] { new Data(DbType.Single, null, value => value.GetOptionalFloat()) }, - new object[] { new Data(DbType.Double, 123.45, value => value.GetDouble()) }, - new object[] { new Data(DbType.Double, 123.45, value => value.GetDouble(), true) }, - new object[] { new Data(DbType.Double, null, value => value.GetOptionalDouble()) }, - new object[] - { - new Data(DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B"), - value => value.GetUuid()) - }, - - new object[] - { - new Data(DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B"), - value => value.GetUuid(), true) - }, - - new object[] { new Data(DbType.Guid, null, value => value.GetOptionalUuid()) }, - new object[] { new Data(DbType.Date, new DateTime(2021, 08, 21), value => value.GetDate()) }, - new object[] - { - new Data(DbType.Date, new DateTime(2021, 08, 21), value => value.GetDate(), true) - }, - - new object[] { new Data(DbType.Date, null, value => value.GetOptionalDate()) }, - new object[] - { - new Data(DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47), - value => value.GetDatetime()) - }, - - new object[] - { - new Data(DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47), - value => value.GetDatetime(), true) - }, - - new object[] { new Data(DbType.DateTime, null, value => value.GetOptionalDatetime()) }, - new object[] - { - new Data(DbType.DateTime2, DateTime.Parse("2029-08-03T06:59:44.8578730Z"), - value => value.GetTimestamp()) - }, - - new object[] - { - new Data(DbType.DateTime2, DateTime.Parse("2029-08-09T17:15:29.6935850Z"), - value => value.GetTimestamp()) - }, - + new object[] { new Data(DbType.Boolean, true) }, + new object[] { new Data(DbType.Boolean, false) }, + new object[] { new Data(DbType.Boolean, true, true) }, + new object[] { new Data(DbType.Boolean, false, true) }, + new object[] { new Data(DbType.Boolean, null) }, + new object[] { new Data(DbType.SByte, -1) }, + new object[] { new Data(DbType.SByte, -2, true) }, + new object[] { new Data(DbType.SByte, null) }, + new object[] { new Data(DbType.Byte, 200) }, + new object[] { new Data(DbType.Byte, 228, true) }, + new object[] { new Data(DbType.Byte, null) }, + new object[] { new Data(DbType.Int16, 14000) }, + new object[] { new Data(DbType.Int16, 14000, true) }, + new object[] { new Data(DbType.Int16, null) }, + new object[] { new Data(DbType.UInt16, 40_000) }, + new object[] { new Data(DbType.UInt16, 40_000, true) }, + new object[] { new Data(DbType.UInt16, null) }, + new object[] { new Data(DbType.Int32, -40_000) }, + new object[] { new Data(DbType.Int32, -40_000, true) }, + new object[] { new Data(DbType.Int32, null) }, + new object[] { new Data(DbType.UInt32, 4_000_000_000) }, + new object[] { new Data(DbType.UInt32, 4_000_000_000, true) }, + new object[] { new Data(DbType.UInt32, null) }, + new object[] { new Data(DbType.Int64, -4_000_000_000) }, + new object[] { new Data(DbType.Int64, -4_000_000_000, true) }, + new object[] { new Data(DbType.Int64, null) }, + new object[] { new Data(DbType.UInt64, 10_000_000_000ul) }, + new object[] { new Data(DbType.UInt64, 10_000_000_000ul, true) }, + new object[] { new Data(DbType.UInt64, null) }, + new object[] { new Data(DbType.Single, -1.7f) }, + new object[] { new Data(DbType.Single, -1.7f, true) }, + new object[] { new Data(DbType.Single, null) }, + new object[] { new Data(DbType.Double, 123.45) }, + new object[] { new Data(DbType.Double, 123.45, true) }, + new object[] { new Data(DbType.Double, null) }, + new object[] { new Data(DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B")) }, + new object[] { new Data(DbType.Guid, new Guid("6E73B41C-4EDE-4D08-9CFB-B7462D9E498B"), true) }, + new object[] { new Data(DbType.Guid, null) }, + new object[] { new Data(DbType.Date, new DateTime(2021, 08, 21)) }, + new object[] { new Data(DbType.Date, new DateTime(2021, 08, 21), true) }, + new object[] { new Data(DbType.Date, null) }, + new object[] { new Data(DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47)) }, + new object[] { new Data(DbType.DateTime, new DateTime(2021, 08, 21, 23, 30, 47), true) }, + new object[] { new Data(DbType.DateTime, null) }, + new object[] { new Data(DbType.DateTime2, DateTime.Parse("2029-08-03T06:59:44.8578730Z")) }, + new object[] { new Data(DbType.DateTime2, DateTime.Parse("2029-08-09T17:15:29.6935850Z")) }, new object[] { new Data(DbType.DateTime2, new DateTime(2021, 08, 21, 23, 30, 47, 581, DateTimeKind.Local), - value => value.GetTimestamp(), true) - }, - - new object[] { new Data(DbType.DateTime2, null, value => value.GetOptionalTimestamp()) }, - new object[] - { - new Data(DbType.Binary, Encoding.ASCII.GetBytes("test str"), - value => value.GetString()) - }, - - new object[] - { - new Data(DbType.Binary, Encoding.ASCII.GetBytes("test str"), - value => value.GetString(), true) + true) }, - - new object[] { new Data(DbType.Binary, null, value => value.GetOptionalString()) }, - new object[] { new Data(DbType.String, "unicode str", value => value.GetUtf8()) }, - new object[] { new Data(DbType.String, "unicode str", value => value.GetUtf8(), true) }, - new object[] { new Data(DbType.String, null, value => value.GetOptionalUtf8()) }, - new object[] { new Data(DbType.Decimal, -18446744073.709551616m, value => value.GetDecimal()) }, - new object[] - { - new Data(DbType.Decimal, -18446744073.709551616m, value => value.GetDecimal(), true) - }, - - new object[] { new Data(DbType.Decimal, null, value => value.GetOptionalDecimal()) } + new object[] { new Data(DbType.DateTime2, null) }, + new object[] { new Data(DbType.Binary, "test str"u8.ToArray()) }, + new object[] { new Data(DbType.Binary, "test str"u8.ToArray(), true) }, + new object[] { new Data(DbType.Binary, null) }, + new object[] { new Data(DbType.String, "unicode str") }, + new object[] { new Data(DbType.String, "unicode str", true) }, + new object[] { new Data(DbType.String, null) }, + new object[] { new Data(DbType.Decimal, -18446744073.709551616m) }, + new object[] { new Data(DbType.Decimal, -18446744073.709551616m, true) }, + new object[] { new Data(DbType.Decimal, null) } ]; public IEnumerator GetEnumerator() => _data.GetEnumerator(); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs index 75be4e27..2ae23c4d 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs @@ -98,45 +98,6 @@ public void YdbParameter_WhenSetDbType_ReturnValueIsConverted() Assert.Equal(1.1f, new YdbParameter("$parameter", DbType.Double) { Value = 1.1f }.TypedValue.Value.DoubleValue); } - [Fact] - public async Task YdbParameter_WhenDateTimeOffset_ReturnTimestamp() - { - var dateTimeOffset = DateTimeOffset.Parse("2029-08-03T06:59:44.8578730Z"); - await using var ydbConnection = await CreateOpenConnectionAsync(); - var tableTableName = $"dateTimeOffset_{Random.Shared.Next()}"; - await new YdbCommand(ydbConnection) - { - CommandText = $"CREATE TABLE {tableTableName} (TimestampField Timestamp, PRIMARY KEY (TimestampField))" - } - .ExecuteNonQueryAsync(); - await new YdbCommand(ydbConnection) - { - CommandText = - $"INSERT INTO {tableTableName}(TimestampField) " + - $"VALUES (@parameter1), (@parameter2), (@parameter3), (@parameter4)", - Parameters = - { - new YdbParameter("$parameter1", dateTimeOffset), - new YdbParameter("$parameter2", DbType.DateTimeOffset, dateTimeOffset.AddHours(1)), - new YdbParameter("$parameter3", DbType.DateTimeOffset, dateTimeOffset.AddHours(2)), - new YdbParameter("$parameter4", YdbDbType.Timestamp, dateTimeOffset.AddHours(3)) - } - } - .ExecuteNonQueryAsync(); - - var ydbDataReader = await new YdbCommand(ydbConnection) { CommandText = $"SELECT * FROM {tableTableName}" } - .ExecuteReaderAsync(); - - var hourCount = 0; - while (ydbDataReader.NextResult()) - { - Assert.True(ydbDataReader.Read()); - Assert.Equal(dateTimeOffset.AddHours(hourCount++).UtcDateTime, ydbDataReader.GetValue(0)); - } - - await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableTableName};" }.ExecuteNonQueryAsync(); - } - [Theory] [InlineData("123e4567-e89b-12d3-a456-426614174000")] [InlineData("2d9e498b-b746-9cfb-084d-de4e1cb4736e")] @@ -261,7 +222,7 @@ public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNull_ReturnsNullValue() public async Task YdbParameter_WhenYdbDbTypeSetAndValueIsNotNull_ReturnsValue() { await using var ydbConnection = await CreateOpenConnectionAsync(); - var tableName = $"NouNull_YdbDbType_{Random.Shared.Next()}"; + var tableName = $"NonNull_YdbDbType_{Random.Shared.Next()}"; await new YdbCommand(ydbConnection) { CommandText = $""" @@ -287,6 +248,10 @@ CustomDecimalColumn Decimal(35, 5) NOT NULL, IntervalColumn Interval NOT NULL, JsonColumn Json NOT NULL, JsonDocumentColumn JsonDocument NOT NULL, + Date32Column Date32 NOT NULL, + Datetime64Column DateTime64 NOT NULL, + Timestamp64Column Timestamp64 NOT NULL, + Interval64Column Interval64 NOT NULL, PRIMARY KEY (Int32Column) ); """ @@ -299,12 +264,14 @@ PRIMARY KEY (Int32Column) Int32Column, BoolColumn, Int64Column, Int16Column, Int8Column, FloatColumn, DoubleColumn, DefaultDecimalColumn, CustomDecimalColumn, Uint8Column, Uint16Column, Uint32Column, Uint64Column, TextColumn, BytesColumn, DateColumn, DatetimeColumn, TimestampColumn, - IntervalColumn, JsonColumn, JsonDocumentColumn + IntervalColumn, JsonColumn, JsonDocumentColumn, Date32Column, Datetime64Column, + Timestamp64Column, Interval64Column ) VALUES ( @Int32Column, @BoolColumn, @Int64Column, @Int16Column, @Int8Column, @FloatColumn, @DoubleColumn, @DefaultDecimalColumn, @CustomDecimalColumn, @Uint8Column, @Uint16Column, @Uint32Column, @Uint64Column, @TextColumn, @BytesColumn, @DateColumn, @DatetimeColumn, - @TimestampColumn, @IntervalColumn, @JsonColumn, @JsonDocumentColumn + @TimestampColumn, @IntervalColumn, @JsonColumn, @JsonDocumentColumn, @Date32Column, + @Datetime64Column, @Timestamp64Column, @Interval64Column ); """, Parameters = @@ -329,7 +296,12 @@ PRIMARY KEY (Int32Column) new YdbParameter("TimestampColumn", YdbDbType.Timestamp, DateTime.UnixEpoch), new YdbParameter("IntervalColumn", YdbDbType.Interval, TimeSpan.Zero), new YdbParameter("JsonColumn", YdbDbType.Json, "{}"), - new YdbParameter("JsonDocumentColumn", YdbDbType.JsonDocument, "{}") + new YdbParameter("JsonDocumentColumn", YdbDbType.JsonDocument, "{}"), + new YdbParameter("Date32Column", YdbDbType.Date32, DateTime.MinValue), + new YdbParameter("Datetime64Column", YdbDbType.Datetime64, DateTime.MinValue), + new YdbParameter("Timestamp64Column", YdbDbType.Timestamp64, DateTime.MinValue), + new YdbParameter("Interval64Column", YdbDbType.Interval64, + TimeSpan.FromMilliseconds(TimeSpan.MinValue.Milliseconds)) } }.ExecuteNonQueryAsync(); @@ -340,7 +312,8 @@ PRIMARY KEY (Int32Column) Int32Column, BoolColumn, Int64Column, Int16Column, Int8Column, FloatColumn, DoubleColumn, DefaultDecimalColumn, CustomDecimalColumn, Uint8Column, Uint16Column, Uint32Column, Uint64Column, TextColumn, BytesColumn, DateColumn, DatetimeColumn, TimestampColumn, - IntervalColumn, JsonColumn, JsonDocumentColumn + IntervalColumn, JsonColumn, JsonDocumentColumn, Date32Column, Datetime64Column, + Timestamp64Column, Interval64Column FROM {tableName}; """ }.ExecuteReaderAsync(); @@ -367,6 +340,10 @@ PRIMARY KEY (Int32Column) Assert.Equal(TimeSpan.Zero, ydbDataReader.GetInterval(18)); Assert.Equal("{}", ydbDataReader.GetJson(19)); Assert.Equal("{}", ydbDataReader.GetJsonDocument(20)); + Assert.Equal(DateTime.MinValue, ydbDataReader.GetDateTime(21)); + Assert.Equal(DateTime.MinValue, ydbDataReader.GetDateTime(22)); + Assert.Equal(DateTime.MinValue, ydbDataReader.GetDateTime(23)); + Assert.Equal(TimeSpan.FromMilliseconds(TimeSpan.MinValue.Milliseconds), ydbDataReader.GetInterval(24)); Assert.False(ydbDataReader.Read()); await ydbDataReader.CloseAsync(); From a19f8bb8f7f76d47b835ab3f6bf143b98b65c94c Mon Sep 17 00:00:00 2001 From: Kirill Kurdyukov Date: Sat, 30 Aug 2025 03:26:46 +0700 Subject: [PATCH 09/48] feat: enforce DECIMAL(p,s) overflow in parameters (#519) Co-authored-by: bogdangalka --- src/Ydb.Sdk/CHANGELOG.md | 2 + .../Ado/Internal/YdbTypedValueExtensions.cs | 42 ++++-- .../src/Ado/Internal/YdbValueExtensions.cs | 18 +-- .../Ydb.Sdk.Ado.Tests/YdbParameterTests.cs | 126 +++++++++++++++--- 4 files changed, 147 insertions(+), 41 deletions(-) diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index be5467e2..ecb8610c 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,5 @@ +- Fix bug wrap-around ADO.NET: Big parameterized Decimal — `((ulong)bits[1] << 32)` -> `((ulong)(uint)bits[1] << 32)` +- Feat ADO.NET: Parameterized Decimal overflow check: `Precision` and `Scale`. - Feat ADO.NET: Deleted support for `DateTimeOffset` was a mistake. - Feat ADO.NET: Added support for `Date32`, `Datetime64`, `Timestamp64` and `Interval64` types in YDB. - Feat: Implement `YdbRetryPolicy` with AWS-inspired Exponential Backoff and Jitter. diff --git a/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs index 31223be9..6f50f078 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbTypedValueExtensions.cs @@ -5,6 +5,17 @@ namespace Ydb.Sdk.Ado.Internal; internal static class YdbTypedValueExtensions { + private const byte MaxPrecisionDecimal = 29; + private static readonly decimal[] Pow10 = CreatePow10(); + + private static decimal[] CreatePow10() + { + var a = new decimal[29]; + a[0] = 1m; + for (var i = 1; i < a.Length; i++) a[i] = a[i - 1] * 10m; // 1..1e28 + return a; + } + internal static TypedValue Null(this Type.Types.PrimitiveTypeId primitiveTypeId) => new() { Type = new Type { OptionalType = new OptionalType { Item = new Type { TypeId = primitiveTypeId } } }, @@ -73,26 +84,29 @@ internal static TypedValue Double(this double value) => internal static TypedValue Decimal(this decimal value, byte precision, byte scale) { - value *= 1.00000000000000000000000000000m; // 29 zeros, max supported by c# decimal - value = Math.Round(value, scale); + if (scale > precision) + throw new ArgumentOutOfRangeException(nameof(scale), "Scale cannot exceed precision"); + + var origScale = (decimal.GetBits(value)[3] >> 16) & 0xFF; + if (origScale > scale || (precision < MaxPrecisionDecimal && Pow10[precision - scale] <= Math.Abs(value))) + { + throw new OverflowException($"Value {value} does not fit Decimal({precision}, {scale})"); + } + + value *= 1.0000000000000000000000000000m; // 28 zeros, max supported by c# decimal + value = Math.Round(value, scale); var bits = decimal.GetBits(value); - var low = ((ulong)bits[1] << 32) + (uint)bits[0]; - var high = (ulong)bits[2]; + var low = ((ulong)(uint)bits[1] << 32) | (uint)bits[0]; + var high = (ulong)(uint)bits[2]; + var isNegative = bits[3] < 0; unchecked { - if (value < 0) + if (isNegative) { - low = ~low; - high = ~high; - - if (low == (ulong)-1L) - { - high += 1; - } - - low += 1; + low = ~low + 1UL; + high = ~high + (low == 0 ? 1UL : 0UL); } } diff --git a/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs index c88fd149..fc25d821 100644 --- a/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs +++ b/src/Ydb.Sdk/src/Ado/Internal/YdbValueExtensions.cs @@ -77,25 +77,21 @@ internal static Guid GetUuid(this Ydb.Value value) internal static decimal GetDecimal(this Ydb.Value value, uint scale) { - var lo = value.Low128; - var hi = value.High128; - var isNegative = (hi & 0x8000_0000_0000_0000UL) != 0; + var low = value.Low128; + var high = value.High128; + var isNegative = (high & 0x8000_0000_0000_0000UL) != 0; unchecked { if (isNegative) { - if (lo == 0) - hi--; - - lo--; - lo = ~lo; - hi = ~hi; + low = ~low + 1UL; + high = ~high + (low == 0 ? 1UL : 0UL); } } - if (hi >> 32 != 0) + if (high >> 32 != 0) throw new OverflowException("Value does not fit into decimal"); - return new decimal((int)lo, (int)(lo >> 32), (int)hi, isNegative, (byte)scale); + return new decimal((int)low, (int)(low >> 32), (int)high, isNegative, (byte)scale); } } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs index 2ae23c4d..afeae156 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterTests.cs @@ -148,12 +148,17 @@ public async Task Date_WhenSetDateOnly_ReturnDateTime() [Theory] [InlineData("12345", "12345.0000000000", 22, 9)] [InlineData("54321", "54321", 5, 0)] - [InlineData("493235.4", "493235.40", 7, 2)] + [InlineData("493235.4", "493235.40", 8, 2)] [InlineData("123.46", "123.46", 5, 2)] + [InlineData("0.46", "0.46", 2, 2)] [InlineData("-184467434073.70911616", "-184467434073.7091161600", 35, 10)] [InlineData("-18446744074", "-18446744074", 12, 0)] [InlineData("-184467440730709551616", "-184467440730709551616", 21, 0)] [InlineData("-218446744073.709551616", "-218446744073.7095516160", 22, 10)] + [InlineData("79228162514264337593543950335", "79228162514264337593543950335", 29, 0)] + [InlineData("79228162514264337593543950.335", "79228162514264337593543950.335", 29, 3)] + [InlineData("-79228162514264337593543950335", "-79228162514264337593543950335", 29, 0)] + [InlineData("-79228162514264337593543950.335", "-79228162514264337593543950.335", 29, 3)] [InlineData(null, null, 22, 9)] [InlineData(null, null, 35, 9)] [InlineData(null, null, 35, 0)] @@ -161,31 +166,120 @@ public async Task Decimal_WhenDecimalIsScaleAndPrecision_ReturnDecimal(string? v byte precision, byte scale) { await using var ydbConnection = await CreateOpenConnectionAsync(); - var decimalTableName = $"DecimalTable_{Random.Shared.Next()}"; + var tableName = $"DecimalTable_{Random.Shared.Next()}"; var decimalValue = value == null ? (decimal?)null : decimal.Parse(value, CultureInfo.InvariantCulture); await new YdbCommand(ydbConnection) - { - CommandText = $""" - CREATE TABLE {decimalTableName} ( - DecimalField Decimal({precision}, {scale}), - PRIMARY KEY (DecimalField) - ) - """ - }.ExecuteNonQueryAsync(); + { CommandText = $"CREATE TABLE {tableName} (d Decimal({precision}, {scale}), PRIMARY KEY (d))" } + .ExecuteNonQueryAsync(); await new YdbCommand(ydbConnection) { - CommandText = $"INSERT INTO {decimalTableName}(DecimalField) VALUES (@DecimalField);", + CommandText = $"INSERT INTO {tableName}(d) VALUES (@d);", Parameters = - { - new YdbParameter("DecimalField", DbType.Decimal, decimalValue) { Precision = precision, Scale = scale } - } + { new YdbParameter("d", DbType.Decimal, decimalValue) { Precision = precision, Scale = scale } } }.ExecuteNonQueryAsync(); Assert.Equal(expected == null ? DBNull.Value : decimal.Parse(expected, CultureInfo.InvariantCulture), - await new YdbCommand(ydbConnection) { CommandText = $"SELECT DecimalField FROM {decimalTableName};" } + await new YdbCommand(ydbConnection) { CommandText = $"SELECT d FROM {tableName};" } .ExecuteScalarAsync()); - await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {decimalTableName};" }.ExecuteNonQueryAsync(); + await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName};" }.ExecuteNonQueryAsync(); + } + + [Theory] + [InlineData("123.456", 5, 2)] + [InlineData("1.46", 2, 2)] + [InlineData("654321", 5, 0)] + [InlineData("493235.4", 7, 2)] + [InlineData("10.46", 3, 2)] + [InlineData("99.999", 5, 2)] + [InlineData("0.001", 3, 2)] + [InlineData("-12.345", 5, 2)] + [InlineData("7.001", 4, 2)] + [InlineData("1.0001", 5, 3)] + [InlineData("1000.00", 5, 2)] + [InlineData("123456.7", 6, 1)] + [InlineData("999.99", 5, 4)] + [InlineData("-100", 2, 0)] + [InlineData("-0.12", 2, 1)] + [InlineData("10.0", 2, 0)] + [InlineData("-0.1", 1, 0)] + [InlineData("10000", 4, 0)] + [InlineData("12345", 4, 0)] + [InlineData("12.3456", 6, 3)] + [InlineData("123.45", 4, 1)] + [InlineData("9999.9", 5, 0)] + [InlineData("-1234.56", 5, 1)] + [InlineData("-1000", 3, 0)] + [InlineData("0.0001", 4, 3)] + [InlineData("99999", 4, 0)] + [InlineData("9.999", 3, 2)] + [InlineData("123.4", 3, 0)] + [InlineData("1.234", 4, 2)] + [InlineData("-98.765", 5, 2)] + [InlineData("100.01", 5, 1)] + [InlineData("100000", 5, 0)] + public async Task Decimal_WhenNotRepresentableBySystemDecimal_ThrowsOverflowException(string value, byte precision, + byte scale) + { + await using var ydbConnection = await CreateOpenConnectionAsync(); + var tableName = $"DecimalOverflowTable__{Random.Shared.Next()}"; + var decimalValue = decimal.Parse(value, CultureInfo.InvariantCulture); + await new YdbCommand(ydbConnection) + { CommandText = $"CREATE TABLE {tableName}(d Decimal(5,2), PRIMARY KEY(d))" } + .ExecuteNonQueryAsync(); + + Assert.Equal($"Value {decimalValue} does not fit Decimal({precision}, {scale})", + (await Assert.ThrowsAsync(() => new YdbCommand(ydbConnection) + { + CommandText = $"INSERT INTO {tableName}(d) VALUES (@d);", + Parameters = + { + new YdbParameter("d", DbType.Decimal, 123.456m) + { Value = decimalValue, Precision = precision, Scale = scale } + } + }.ExecuteNonQueryAsync())).Message); + + Assert.Equal(0ul, + (ulong)(await new YdbCommand(ydbConnection) { CommandText = $"SELECT COUNT(*) FROM {tableName};" } + .ExecuteScalarAsync())! + ); + + await new YdbCommand(ydbConnection) { CommandText = $"DROP TABLE {tableName};" }.ExecuteNonQueryAsync(); + } + + + [Fact] + public void Decimal_WhenScaleGreaterThanPrecision_ThrowsArgumentOutOfRangeException() => + Assert.Throws(() => + new YdbParameter("d", DbType.Decimal, 0.0m) { Precision = 1, Scale = 2 }.TypedValue); + + [Theory] + [InlineData("10000000000000000000000000000000000", 35, 0)] + [InlineData("1000000000000000000000000.0000000000", 35, 10)] + [InlineData("1000000000000000000000000000000000", 34, 0)] + [InlineData("100000000000000000000000.0000000000", 34, 10)] + [InlineData("100000000000000000000000000000000", 33, 0)] + [InlineData("10000000000000000000000.0000000000", 33, 10)] + [InlineData("-10000000000000000000000000000000", 32, 0)] + [InlineData("-1000000000000000000000.0000000000", 32, 10)] + [InlineData("-1000000000000000000000000000000", 31, 0)] + [InlineData("-100000000000000000000.0000000000", 31, 10)] + [InlineData("1000000000000000000000000000000", 30, 0)] + [InlineData("100000000000000000000.0000000000", 30, 10)] + [InlineData("79228162514264337593543950336", 29, 0)] + [InlineData("79228162514264337593543950.336", 29, 3)] + [InlineData("-79228162514264337593543950336", 29, 0)] + [InlineData("-79228162514264337593543950.336", 29, 3)] + [InlineData("100000", 4, 0)] // inf + public async Task Decimal_WhenYdbReturnsDecimalWithPrecisionGreaterThan28_ThrowsOverflowException(string value, + int precision, int scale) + { + await using var ydbConnection = await CreateOpenConnectionAsync(); + Assert.Equal("Value does not fit into decimal", (await Assert.ThrowsAsync(() => + new YdbCommand(ydbConnection) + { CommandText = $"SELECT (CAST('{value}' AS Decimal({precision}, {scale})));" } + .ExecuteScalarAsync()) + ).Message); } [Fact] From 4ac1c016ef3705e2baf67b942e49a604ade21985 Mon Sep 17 00:00:00 2001 From: Kirill Kurdyukov Date: Mon, 1 Sep 2025 09:31:16 +0700 Subject: [PATCH 10/48] Feat ADO.NET: cache gRPC transport by `gRPCConnectionString` to reuse channels. (#520) * Feat ADO.NET: cache gRPC transport by `gRPCConnectionString` to reuse channels. * refactoring * fix session leak in tests --- src/Ydb.Sdk/CHANGELOG.md | 3 +- src/Ydb.Sdk/src/Ado/PoolManager.cs | 55 ++------ .../src/Ado/Session/PoolingSessionFactory.cs | 8 +- .../src/Ado/YdbConnectionStringBuilder.cs | 16 --- src/Ydb.Sdk/src/IDriver.cs | 10 +- .../BulkUpsertImporterBenchmark.cs | 4 + .../Ydb.Sdk.Ado.Tests/PoolManagerTests.cs | 71 ++++++++++ .../Session/PoolingSessionTests.cs | 4 +- .../test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs | 1 - .../YdbConnectionStringBuilderTests.cs | 7 +- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 126 +++++++++--------- .../YdbParameterCollectionTests.cs | 3 +- .../test/Ydb.Sdk.Ado.Tests/YdbSchemaTests.cs | 108 +++++++-------- 13 files changed, 220 insertions(+), 196 deletions(-) create mode 100644 src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index ecb8610c..336f08a8 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,4 +1,5 @@ -- Fix bug wrap-around ADO.NET: Big parameterized Decimal — `((ulong)bits[1] << 32)` -> `((ulong)(uint)bits[1] << 32)` +- Feat ADO.NET: cache gRPC transport by `gRPCConnectionString` to reuse channels. +- Fix bug wrap-around ADO.NET: Big parameterized Decimal — `((ulong)bits[1] << 32)` -> `((ulong)(uint)bits[1] << 32)`. - Feat ADO.NET: Parameterized Decimal overflow check: `Precision` and `Scale`. - Feat ADO.NET: Deleted support for `DateTimeOffset` was a mistake. - Feat ADO.NET: Added support for `Date32`, `Datetime64`, `Timestamp64` and `Interval64` types in YDB. diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index 6b594060..84606558 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -6,9 +6,7 @@ namespace Ydb.Sdk.Ado; internal static class PoolManager { private static readonly SemaphoreSlim SemaphoreSlim = new(1); // async mutex - private static readonly ConcurrentDictionary Pools = new(); - private static readonly ConcurrentDictionary ImplicitPools = new(); internal static async Task GetSession( YdbConnectionStringBuilder settings, @@ -43,58 +41,29 @@ await PoolingSessionFactory.Create(settings), settings } } - internal static ISession GetImplicitSession(YdbConnectionStringBuilder settings) + internal static async Task ClearPool(string connectionString) { - if (ImplicitPools.TryGetValue(settings.ConnectionString, out var ready)) - return ready.OpenSession(CancellationToken.None).GetAwaiter().GetResult(); - - var driver = settings.BuildDriver().GetAwaiter().GetResult(); - ISessionSource source; - - SemaphoreSlim.Wait(); - try + if (Pools.Remove(connectionString, out var sessionPool)) { - if (!ImplicitPools.TryGetValue(settings.ConnectionString, out source)) + try { - source = new ImplicitSessionSource(driver); - ImplicitPools[settings.ConnectionString] = source; - driver = null; + await SemaphoreSlim.WaitAsync(); + + await sessionPool.DisposeAsync(); + } + finally + { + SemaphoreSlim.Release(); } } - finally - { - SemaphoreSlim.Release(); - if (driver != null) - driver.DisposeAsync().GetAwaiter().GetResult(); - } - - return source.OpenSession(CancellationToken.None).GetAwaiter().GetResult(); - } - - internal static async Task ClearPool(string connectionString) - { - Pools.TryRemove(connectionString, out var pooled); - ImplicitPools.TryRemove(connectionString, out var implicitSrc); - - var tasks = new List(2); - if (pooled != null) tasks.Add(pooled.DisposeAsync().AsTask()); - if (implicitSrc != null) tasks.Add(implicitSrc.DisposeAsync().AsTask()); - - if (tasks.Count > 0) - await Task.WhenAll(tasks); } internal static async Task ClearAllPools() { - var pooled = Pools.ToArray(); - var implicitArr = ImplicitPools.ToArray(); + var keys = Pools.Keys.ToList(); - Pools.Clear(); - ImplicitPools.Clear(); + var tasks = keys.Select(ClearPool).ToList(); - var tasks = new List(pooled.Length + implicitArr.Length); - tasks.AddRange(pooled.Select(kv => kv.Value.DisposeAsync().AsTask())); - tasks.AddRange(implicitArr.Select(kv => kv.Value.DisposeAsync().AsTask())); await Task.WhenAll(tasks); } } diff --git a/src/Ydb.Sdk/src/Ado/Session/PoolingSessionFactory.cs b/src/Ydb.Sdk/src/Ado/Session/PoolingSessionFactory.cs index 9019c658..7c6420b6 100644 --- a/src/Ydb.Sdk/src/Ado/Session/PoolingSessionFactory.cs +++ b/src/Ydb.Sdk/src/Ado/Session/PoolingSessionFactory.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; namespace Ydb.Sdk.Ado.Session; @@ -9,16 +8,13 @@ internal class PoolingSessionFactory : IPoolingSessionFactory private readonly bool _disableServerBalancer; private readonly ILogger _logger; - internal PoolingSessionFactory(IDriver driver, YdbConnectionStringBuilder settings, ILoggerFactory loggerFactory) + internal PoolingSessionFactory(IDriver driver, YdbConnectionStringBuilder settings) { _driver = driver; _disableServerBalancer = settings.DisableServerBalancer; - _logger = loggerFactory.CreateLogger(); + _logger = settings.LoggerFactory.CreateLogger(); } - public static async Task Create(YdbConnectionStringBuilder settings) => - new(await settings.BuildDriver(), settings, settings.LoggerFactory ?? NullLoggerFactory.Instance); - public PoolingSession NewSession(PoolingSessionSource source) => new(_driver, source, _disableServerBalancer, _logger); diff --git a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs index b2305b38..3b3a480c 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs @@ -40,7 +40,6 @@ private void InitDefaultValues() _maxReceiveMessageSize = GrpcDefaultSettings.MaxReceiveMessageSize; _disableDiscovery = GrpcDefaultSettings.DisableDiscovery; _disableServerBalancer = false; - _enableImplicitSession = false; } public string Host @@ -315,18 +314,6 @@ public int CreateSessionTimeout private int _createSessionTimeout; - public bool EnableImplicitSession - { - get => _enableImplicitSession; - set - { - _enableImplicitSession = value; - SaveValue(nameof(EnableImplicitSession), value); - } - } - - private bool _enableImplicitSession; - public ILoggerFactory? LoggerFactory { get; init; } public ICredentialsProvider? CredentialsProvider { get; init; } @@ -503,9 +490,6 @@ static YdbConnectionOption() AddOption(new YdbConnectionOption(BoolExtractor, (builder, disableServerBalancer) => builder.DisableServerBalancer = disableServerBalancer), "DisableServerBalancer", "Disable Server Balancer"); - AddOption(new YdbConnectionOption(BoolExtractor, - (builder, enableImplicit) => builder.EnableImplicitSession = enableImplicit), - "EnableImplicitSession", "ImplicitSession"); } private static void AddOption(YdbConnectionOption option, params string[] keys) diff --git a/src/Ydb.Sdk/src/IDriver.cs b/src/Ydb.Sdk/src/IDriver.cs index 9599284f..19ae9d08 100644 --- a/src/Ydb.Sdk/src/IDriver.cs +++ b/src/Ydb.Sdk/src/IDriver.cs @@ -31,6 +31,10 @@ public ValueTask> BidirectionalStreamC where TResponse : class; ILoggerFactory LoggerFactory { get; } + + void RegisterOwner(); + + bool IsDisposed { get; } } public interface IBidirectionalStream : IDisposable @@ -63,6 +67,8 @@ public abstract class BaseDriver : IDriver internal readonly GrpcChannelFactory GrpcChannelFactory; internal readonly ChannelPool ChannelPool; + private int _ownerCount; + protected int Disposed; internal BaseDriver( @@ -204,12 +210,14 @@ protected async ValueTask GetCallOptions(GrpcRequestSettings settin } public ILoggerFactory LoggerFactory { get; } + public void RegisterOwner() => _ownerCount++; + public bool IsDisposed => Disposed == 1; public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); public async ValueTask DisposeAsync() { - if (Interlocked.CompareExchange(ref Disposed, 1, 0) == 0) + if (--_ownerCount <= 0 && Interlocked.CompareExchange(ref Disposed, 1, 0) == 0) { await ChannelPool.DisposeAsync(); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Benchmarks/BulkUpsertImporterBenchmark.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Benchmarks/BulkUpsertImporterBenchmark.cs index 06fbc0e7..27d450a6 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Benchmarks/BulkUpsertImporterBenchmark.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Benchmarks/BulkUpsertImporterBenchmark.cs @@ -63,4 +63,8 @@ public Task NotImplementedException(); public ILoggerFactory LoggerFactory => null!; + + public void RegisterOwner() => throw new NotImplementedException(); + + public bool IsDisposed => false; } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs new file mode 100644 index 00000000..562f3b04 --- /dev/null +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs @@ -0,0 +1,71 @@ +using System.Collections.Immutable; +using Xunit; + +namespace Ydb.Sdk.Ado.Tests; + +[Collection("PoolManagerTests")] +[CollectionDefinition("PoolManagerTests", DisableParallelization = true)] +public class PoolManagerTests +{ + [Theory] + [InlineData(new[] + { + "MinSessionSize=1", "MinSessionSize=2", "MinSessionSize=3", + "MinSessionSize=1;DisableDiscovery=True", "MinSessionSize=2;DisableDiscovery=True" + }, 2, 5)] // 2 transports (by the DisableDiscovery flag), 5 pools + [InlineData( + new[] { "MinSessionSize=1", "MinSessionSize=2", "MinSessionSize=3", "MinSessionSize=4", "MinSessionSize=5" }, + 1, 5)] // 1 transport, 5 five pools + [InlineData(new[] + { "MinSessionSize=1", "MinSessionSize=1", "MinSessionSize=2", "MinSessionSize=2", "MinSessionSize=3" }, 1, + 3)] // duplicate rows — we expect 1 transport, 3 pools + [InlineData(new[] + { + "MinSessionSize=1;ConnectTimeout=5", "MinSessionSize=1;ConnectTimeout=6", "MinSessionSize=1;ConnectTimeout=7", + "MinSessionSize=1;ConnectTimeout=8", "MinSessionSize=1;ConnectTimeout=9" + }, 5, 5)] // 5 transport, 5 five pools + [InlineData(new[] { "MinSessionSize=1" }, 1, 1)] // simple case + public async Task PoolManager_CachingAndCleanup(string[] connectionStrings, int expectedDrivers, int expectedPools) + { + await YdbConnection.ClearAllPools(); + PoolManager.Drivers.Clear(); + + var connections = connectionStrings + .Select(connectionString => new YdbConnection(connectionString)) + .ToImmutableArray(); + var parallelTasks = connections.Select(connection => connection.OpenAsync()).ToList(); + await Task.WhenAll(parallelTasks); + + Assert.Equal(expectedDrivers, PoolManager.Drivers.Count); + Assert.Equal(expectedPools, PoolManager.Pools.Count); + + await ClearAllConnections(connections); + + parallelTasks = connections.Select(connection => connection.OpenAsync()).ToList(); + await Task.WhenAll(parallelTasks); + + foreach (var (_, driver) in PoolManager.Drivers) + { + Assert.False(driver.IsDisposed); + } + + Assert.Equal(expectedDrivers, PoolManager.Drivers.Count); + Assert.Equal(expectedPools, PoolManager.Pools.Count); + + await ClearAllConnections(connections); + } + + private static async Task ClearAllConnections(IReadOnlyCollection connections) + { + foreach (var connection in connections) + await connection.CloseAsync(); + + await YdbConnection.ClearAllPools(); + Assert.Empty(PoolManager.Pools); + + foreach (var (_, driver) in PoolManager.Drivers) + { + Assert.True(driver.IsDisposed); + } + } +} diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/PoolingSessionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/PoolingSessionTests.cs index fcbdd3ca..281b77a3 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/PoolingSessionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/PoolingSessionTests.cs @@ -21,7 +21,7 @@ public class PoolingSessionTests public PoolingSessionTests() { - var settings = new YdbConnectionStringBuilder(); + var settings = new YdbConnectionStringBuilder { LoggerFactory = TestUtils.LoggerFactory }; _mockIDriver = new Mock(MockBehavior.Strict); _mockIDriver.Setup(driver => driver.LoggerFactory).Returns(TestUtils.LoggerFactory); @@ -31,7 +31,7 @@ public PoolingSessionTests() It.Is(grpcRequestSettings => grpcRequestSettings.NodeId == NodeId)) ).ReturnsAsync(_mockAttachStream.Object); _mockAttachStream.Setup(stream => stream.Dispose()); - _poolingSessionFactory = new PoolingSessionFactory(_mockIDriver.Object, settings, TestUtils.LoggerFactory); + _poolingSessionFactory = new PoolingSessionFactory(_mockIDriver.Object, settings); _poolingSessionSource = new PoolingSessionSource(_poolingSessionFactory, settings); } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs index 05ca0c1c..e322da34 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs @@ -67,7 +67,6 @@ public async Task ExecuteReaderAsync_WhenSelectNull_ThrowFieldIsNull() dbCommand.CommandText = "SELECT NULL"; var reader = await dbCommand.ExecuteReaderAsync(); await reader.ReadAsync(); - Assert.Equal("Field is null.", Assert.Throws(() => reader.GetFloat(0)).Message); } 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 386c71d6..736b1e96 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs @@ -28,7 +28,6 @@ public void InitDefaultValues_WhenEmptyConstructorInvoke_ReturnDefaultConnection Assert.False(ydbConnectionStringBuilder.DisableDiscovery); Assert.False(ydbConnectionStringBuilder.DisableServerBalancer); Assert.False(ydbConnectionStringBuilder.UseTls); - Assert.False(ydbConnectionStringBuilder.EnableImplicitSession); } [Fact] @@ -51,7 +50,7 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=true;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=true;DisableServerBalancer=true;EnableImplicitSession=true;" + "DisableDiscovery=true;DisableServerBalancer=true;" ); Assert.Equal(2135, connectionString.Port); @@ -75,11 +74,9 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=True;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=True;DisableServerBalancer=True;EnableImplicitSession=True", - connectionString.ConnectionString); + "DisableDiscovery=True;DisableServerBalancer=True", connectionString.ConnectionString); Assert.True(connectionString.DisableDiscovery); Assert.True(connectionString.DisableServerBalancer); - Assert.True(connectionString.EnableImplicitSession); } [Fact] diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index f51d0b0c..1578eed3 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -106,44 +106,44 @@ public async Task ClosedYdbDataReader_WhenConnectionIsClosed_ThrowException() [Fact] public async Task SetNulls_WhenTableAllTypes_SussesSet() { - var ydbConnection = await CreateOpenConnectionAsync(); + await using var ydbConnection = await CreateOpenConnectionAsync(); var ydbCommand = ydbConnection.CreateCommand(); var tableName = "AllTypes_" + Random.Shared.Next(); - ydbCommand.CommandText = @$" -CREATE TABLE {tableName} ( - id INT32, - bool_column BOOL, - bigint_column INT64, - smallint_column INT16, - tinyint_column INT8, - float_column FLOAT, - double_column DOUBLE, - decimal_column DECIMAL(22,9), - uint8_column UINT8, - uint16_column UINT16, - uint32_column UINT32, - uint64_column UINT64, - text_column TEXT, - binary_column BYTES, - json_column JSON, - jsondocument_column JSONDOCUMENT, - date_column DATE, - datetime_column DATETIME, - timestamp_column TIMESTAMP, - interval_column INTERVAL, - PRIMARY KEY (id) -) -"; + ydbCommand.CommandText = $""" + CREATE TABLE {tableName} ( + id INT32, + bool_column BOOL, + bigint_column INT64, + smallint_column INT16, + tinyint_column INT8, + float_column FLOAT, + double_column DOUBLE, + decimal_column DECIMAL(22,9), + uint8_column UINT8, + uint16_column UINT16, + uint32_column UINT32, + uint64_column UINT64, + text_column TEXT, + binary_column BYTES, + json_column JSON, + jsondocument_column JSONDOCUMENT, + date_column DATE, + datetime_column DATETIME, + timestamp_column TIMESTAMP, + interval_column INTERVAL, + PRIMARY KEY (id) + ) + """; await ydbCommand.ExecuteNonQueryAsync(); - ydbCommand.CommandText = @$" -INSERT INTO {tableName} - (id, bool_column, bigint_column, smallint_column, tinyint_column, float_column, double_column, decimal_column, - uint8_column, uint16_column, uint32_column, uint64_column, text_column, binary_column, json_column, - jsondocument_column, date_column, datetime_column, timestamp_column, interval_column) VALUES -(@name1, @name2, @name3, @name4, @name5, @name6, @name7, @name8, @name9, @name10, @name11, @name12, @name13, @name14, - @name15, @name16, @name17, @name18, @name19, @name20); -"; + ydbCommand.CommandText = + $""" + INSERT INTO {tableName} (id, bool_column, bigint_column, smallint_column, tinyint_column, float_column, + double_column, decimal_column, uint8_column, uint16_column, uint32_column, uint64_column, text_column, + binary_column, json_column, jsondocument_column, date_column, datetime_column, timestamp_column, + interval_column) VALUES (@name1, @name2, @name3, @name4, @name5, @name6, @name7, @name8, @name9, @name10, + @name11, @name12, @name13, @name14, @name15, @name16, @name17, @name18, @name19, @name20); + """; ydbCommand.Parameters.Add(new YdbParameter { ParameterName = "name1", DbType = DbType.Int32, Value = null }); ydbCommand.Parameters.Add(new YdbParameter { ParameterName = "name2", DbType = DbType.Boolean, Value = null }); @@ -198,7 +198,7 @@ public async Task OpenAsync_WhenCancelTokenIsCanceled_ThrowYdbException() await using var connection = CreateConnection(); connection.ConnectionString = ConnectionString + ";MinSessionPool=1"; using var cts = new CancellationTokenSource(); - cts.Cancel(); + await cts.CancelAsync(); await Assert.ThrowsAnyAsync(async () => await connection.OpenAsync(cts.Token)); Assert.Equal(ConnectionState.Closed, connection.State); } @@ -210,7 +210,7 @@ public async Task YdbDataReader_WhenCancelTokenIsCanceled_ThrowYdbException() var command = new YdbCommand(connection) { CommandText = "SELECT 1; SELECT 1; SELECT 1;" }; var ydbDataReader = await command.ExecuteReaderAsync(); using var cts = new CancellationTokenSource(); - cts.Cancel(); + await cts.CancelAsync(); await ydbDataReader.ReadAsync(cts.Token); // first part in memory Assert.False(ydbDataReader.IsClosed); @@ -220,6 +220,8 @@ public async Task YdbDataReader_WhenCancelTokenIsCanceled_ThrowYdbException() (await Assert.ThrowsAsync(async () => await ydbDataReader.NextResultAsync(cts.Token))).Code); Assert.True(ydbDataReader.IsClosed); Assert.Equal(ConnectionState.Broken, connection.State); + // CLOSE OLD CONNECTION! (return to pool) + await connection.CloseAsync(); // ReSharper disable once MethodSupportsCancellation await connection.OpenAsync(); @@ -270,7 +272,7 @@ public async Task ExecuteReaderAsync_WhenExecutedYdbDataReaderThenCancelTokenIsC await ydbDataReader.ReadAsync(cts.Token); Assert.Equal(1, ydbDataReader.GetValue(0)); Assert.True(await ydbDataReader.NextResultAsync(cts.Token)); - cts.Cancel(); + await cts.CancelAsync(); await ydbDataReader.ReadAsync(cts.Token); Assert.Equal(1, ydbDataReader.GetValue(0)); // ReSharper disable once MethodSupportsCancellation @@ -305,10 +307,10 @@ public async Task BulkUpsertImporter_HappyPath_Add_Flush() { var tableName = $"BulkImporter_{Guid.NewGuid():N}"; - await using var conn = await CreateOpenConnectionAsync(); + await using var ydbConnection = await CreateOpenConnectionAsync(); try { - await using (var createCmd = conn.CreateCommand()) + await using (var createCmd = ydbConnection.CreateCommand()) { createCmd.CommandText = $""" CREATE TABLE {tableName} ( @@ -322,25 +324,25 @@ PRIMARY KEY (Id) var columns = new[] { "Id", "Name" }; - var importer = conn.BeginBulkUpsertImport(tableName, columns); + var importer = ydbConnection.BeginBulkUpsertImport(tableName, columns); await importer.AddRowAsync(1, "Alice"); await importer.AddRowAsync(2, "Bob"); await importer.FlushAsync(); - await using (var checkCmd = conn.CreateCommand()) + await using (var checkCmd = ydbConnection.CreateCommand()) { checkCmd.CommandText = $"SELECT COUNT(*) FROM {tableName}"; var count = Convert.ToInt32(await checkCmd.ExecuteScalarAsync()); Assert.Equal(2, count); } - importer = conn.BeginBulkUpsertImport(tableName, columns); + importer = ydbConnection.BeginBulkUpsertImport(tableName, columns); await importer.AddRowAsync(3, "Charlie"); await importer.AddRowAsync(4, "Diana"); await importer.FlushAsync(); - await using (var checkCmd = conn.CreateCommand()) + await using (var checkCmd = ydbConnection.CreateCommand()) { checkCmd.CommandText = $"SELECT Name FROM {tableName} ORDER BY Id"; var names = new List(); @@ -355,7 +357,7 @@ PRIMARY KEY (Id) } finally { - await using var dropCmd = conn.CreateCommand(); + await using var dropCmd = ydbConnection.CreateCommand(); dropCmd.CommandText = $"DROP TABLE {tableName}"; await dropCmd.ExecuteNonQueryAsync(); } @@ -365,31 +367,25 @@ PRIMARY KEY (Id) public async Task BulkUpsertImporter_ThrowsOnInvalidRowCount() { var tableName = $"BulkImporter_{Guid.NewGuid():N}"; - await using var conn = await CreateOpenConnectionAsync(); + await using var ydbConnection = await CreateOpenConnectionAsync(); try { - await using (var createCmd = conn.CreateCommand()) + await using (var createCmd = ydbConnection.CreateCommand()) { - createCmd.CommandText = $""" - CREATE TABLE {tableName} ( - Id Int32, - Name Utf8, - PRIMARY KEY (Id) - ) - """; + createCmd.CommandText = $"CREATE TABLE {tableName} (Id Int32, Name Utf8, PRIMARY KEY (Id))"; await createCmd.ExecuteNonQueryAsync(); } var columns = new[] { "Id", "Name" }; - var importer = conn.BeginBulkUpsertImport(tableName, columns); + var importer = ydbConnection.BeginBulkUpsertImport(tableName, columns); await Assert.ThrowsAsync(async () => await importer.AddRowAsync(1)); await Assert.ThrowsAsync(async () => await importer.AddRowAsync(2)); } finally { - await using var dropCmd = conn.CreateCommand(); + await using var dropCmd = ydbConnection.CreateCommand(); dropCmd.CommandText = $"DROP TABLE {tableName}"; await dropCmd.ExecuteNonQueryAsync(); } @@ -401,12 +397,12 @@ public async Task BulkUpsertImporter_MultipleImporters_Parallel() var table1 = $"BulkImporter_{Guid.NewGuid():N}_1"; var table2 = $"BulkImporter_{Guid.NewGuid():N}_2"; - var conn = await CreateOpenConnectionAsync(); + await using var ydbConnection = await CreateOpenConnectionAsync(); try { foreach (var table in new[] { table1, table2 }) { - await using var createCmd = conn.CreateCommand(); + await using var createCmd = ydbConnection.CreateCommand(); createCmd.CommandText = $""" CREATE TABLE {table} ( Id Int32, @@ -422,7 +418,8 @@ PRIMARY KEY (Id) await Task.WhenAll( Task.Run(async () => { - var importer = conn.BeginBulkUpsertImport(table1, columns); + // ReSharper disable once AccessToDisposedClosure + var importer = ydbConnection.BeginBulkUpsertImport(table1, columns); var rows = Enumerable.Range(0, 20) .Select(i => new object[] { i, $"A{i}" }) .ToArray(); @@ -432,7 +429,8 @@ await Task.WhenAll( }), Task.Run(async () => { - var importer = conn.BeginBulkUpsertImport(table2, columns); + // ReSharper disable once AccessToDisposedClosure + var importer = ydbConnection.BeginBulkUpsertImport(table2, columns); var rows = Enumerable.Range(0, 20) .Select(i => new object[] { i, $"B{i}" }) .ToArray(); @@ -444,7 +442,7 @@ await Task.WhenAll( foreach (var table in new[] { table1, table2 }) { - await using var checkCmd = conn.CreateCommand(); + await using var checkCmd = ydbConnection.CreateCommand(); checkCmd.CommandText = $"SELECT COUNT(*) FROM {table}"; var count = Convert.ToInt32(await checkCmd.ExecuteScalarAsync()); Assert.Equal(20, count); @@ -454,12 +452,10 @@ await Task.WhenAll( { foreach (var table in new[] { table1, table2 }) { - await using var dropCmd = conn.CreateCommand(); + await using var dropCmd = ydbConnection.CreateCommand(); dropCmd.CommandText = $"DROP TABLE {table}"; await dropCmd.ExecuteNonQueryAsync(); } - - await conn.DisposeAsync(); } } @@ -467,11 +463,11 @@ await Task.WhenAll( public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() { var tableName = $"Nonexistent_{Guid.NewGuid():N}"; - await using var conn = await CreateOpenConnectionAsync(); + await using var ydbConnection = await CreateOpenConnectionAsync(); var columns = new[] { "Id", "Name" }; - var importer = conn.BeginBulkUpsertImport(tableName, columns); + var importer = ydbConnection.BeginBulkUpsertImport(tableName, columns); await importer.AddRowAsync(1, "NotExists"); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterCollectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterCollectionTests.cs index 1e95206e..ccfc871b 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterCollectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbParameterCollectionTests.cs @@ -11,8 +11,7 @@ public class YdbParameterCollectionTests public YdbParameterCollectionTests() { - _ydbParameterCollection = new YdbParameterCollection(); - + _ydbParameterCollection = []; _ydbParameterCollection.AddWithValue("$param1", 1); _ydbParameterCollection.AddWithValue("$param2", 1.0); _ydbParameterCollection.AddWithValue("$param3", DbType.String, "text"); diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbSchemaTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbSchemaTests.cs index 7ca6397c..6ab593df 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbSchemaTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbSchemaTests.cs @@ -221,53 +221,53 @@ protected override async Task OnInitializeAsync() await new YdbCommand(ydbConnection) { - CommandText = $@" - CREATE TABLE `{_table1}` (a Int32 NOT NULL, b Int32, PRIMARY KEY(a)); - CREATE TABLE `{_table2}` (a Int32 NOT NULL, b Int32, PRIMARY KEY(a)); - CREATE TABLE `{_table3}` (a Int32 NOT NULL, b Int32, PRIMARY KEY(a)); - - CREATE TABLE {_allTypesTable} ( - Int32Column Int32 NOT NULL, - BoolColumn Bool NOT NULL, - Int64Column Int64 NOT NULL, - Int16Column Int16 NOT NULL, - Int8Column Int8 NOT NULL, - FloatColumn Float NOT NULL, - DoubleColumn Double NOT NULL, - DefaultDecimalColumn Decimal(22,9) NOT NULL, - Uint8Column Uint8 NOT NULL, - Uint16Column Uint16 NOT NULL, - Uint32Column Uint32 NOT NULL, - Uint64Column Uint64 NOT NULL, - TextColumn Text NOT NULL, - BytesColumn Bytes NOT NULL, - DateColumn Date NOT NULL, - DatetimeColumn Datetime NOT NULL, - TimestampColumn Timestamp NOT NULL, - PRIMARY KEY (Int32Column) - ); - - CREATE TABLE {_allTypesTableNullable} ( - Int32Column Int32, - BoolColumn Bool, - Int64Column Int64, - Int16Column Int16, - Int8Column Int8, - FloatColumn Float, - DoubleColumn Double, - DefaultDecimalColumn Decimal(22,9), - Uint8Column Uint8, - Uint16Column Uint16, - Uint32Column Uint32, - Uint64Column Uint64, - TextColumn Text, - BytesColumn Bytes, - DateColumn Date, - DatetimeColumn Datetime, - TimestampColumn Timestamp, - PRIMARY KEY (Int32Column) - ); - " + CommandText = $""" + CREATE TABLE `{_table1}` (a Int32 NOT NULL, b Int32, PRIMARY KEY(a)); + CREATE TABLE `{_table2}` (a Int32 NOT NULL, b Int32, PRIMARY KEY(a)); + CREATE TABLE `{_table3}` (a Int32 NOT NULL, b Int32, PRIMARY KEY(a)); + + CREATE TABLE {_allTypesTable} ( + Int32Column Int32 NOT NULL, + BoolColumn Bool NOT NULL, + Int64Column Int64 NOT NULL, + Int16Column Int16 NOT NULL, + Int8Column Int8 NOT NULL, + FloatColumn Float NOT NULL, + DoubleColumn Double NOT NULL, + DefaultDecimalColumn Decimal(22,9) NOT NULL, + Uint8Column Uint8 NOT NULL, + Uint16Column Uint16 NOT NULL, + Uint32Column Uint32 NOT NULL, + Uint64Column Uint64 NOT NULL, + TextColumn Text NOT NULL, + BytesColumn Bytes NOT NULL, + DateColumn Date NOT NULL, + DatetimeColumn Datetime NOT NULL, + TimestampColumn Timestamp NOT NULL, + PRIMARY KEY (Int32Column) + ); + + CREATE TABLE {_allTypesTableNullable} ( + Int32Column Int32, + BoolColumn Bool, + Int64Column Int64, + Int16Column Int16, + Int8Column Int8, + FloatColumn Float, + DoubleColumn Double, + DefaultDecimalColumn Decimal(22,9), + Uint8Column Uint8, + Uint16Column Uint16, + Uint32Column Uint32, + Uint64Column Uint64, + TextColumn Text, + BytesColumn Bytes, + DateColumn Date, + DatetimeColumn Datetime, + TimestampColumn Timestamp, + PRIMARY KEY (Int32Column) + ); + """ }.ExecuteNonQueryAsync(); } @@ -277,13 +277,13 @@ protected override async Task OnDisposeAsync() await new YdbCommand(ydbConnection) { - CommandText = $@" - DROP TABLE `{_table1}`; - DROP TABLE `{_table2}`; - DROP TABLE `{_table3}`; - DROP TABLE `{_allTypesTable}`; - DROP TABLE `{_allTypesTableNullable}`; - " + CommandText = $""" + DROP TABLE `{_table1}`; + DROP TABLE `{_table2}`; + DROP TABLE `{_table3}`; + DROP TABLE `{_allTypesTable}`; + DROP TABLE `{_allTypesTableNullable}`; + """ }.ExecuteNonQueryAsync(); } } From f7da9187d71970ba02439d1aa97c06457ed79891 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Tue, 12 Aug 2025 19:48:38 +0300 Subject: [PATCH 11/48] feat: add EnableImplicitSession flag support with parsing tests --- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 5 + .../src/Ado/YdbConnectionStringBuilder.cs | 5 +- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 174 ------------------ 3 files changed, 8 insertions(+), 176 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 7815d307..d32aeaab 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -38,6 +38,11 @@ internal ISession Session } private ISession _session = null!; + + internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; + + internal ISession GetExecutionSession(bool useImplicit) + => useImplicit ? new ImplicitSession(Session.Driver) : Session; internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; diff --git a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs index 3b3a480c..d135d1ea 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Ydb.Sdk.Auth; +using Ydb.Sdk.Pool; using Ydb.Sdk.Transport; namespace Ydb.Sdk.Ado; @@ -28,8 +29,8 @@ private void InitDefaultValues() _port = 2136; _database = "/local"; _minSessionPool = 0; - _maxSessionPool = 100; - _createSessionTimeout = 5; + _maxSessionPool = SessionPoolDefaultSettings.MaxSessionPool; + _createSessionTimeout = SessionPoolDefaultSettings.CreateSessionTimeoutSeconds; _sessionIdleTimeout = 300; _useTls = false; _connectTimeout = GrpcDefaultSettings.ConnectTimeoutSeconds; diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index 1578eed3..3cf2d07a 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -473,178 +473,4 @@ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() await Assert.ThrowsAsync(async () => { await importer.FlushAsync(); }); } - - [Fact] - public async Task EnableImplicitSession_WhenTrue_AndNoTransaction_UsesImplicitSession() - { - var cs = ConnectionString + ";EnableImplicitSession=true"; - - await using var conn = new YdbConnection(cs); - await conn.OpenAsync(); - - var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT 1"; - var result = Convert.ToInt64(await cmd.ExecuteScalarAsync()); - Assert.Equal(1L, result); - - var implicitSession = conn.GetExecutionSession(useImplicit: true); - var pooledSession = conn.GetExecutionSession(useImplicit: false); - Assert.NotEqual(implicitSession, pooledSession); - } - - [Fact] - public async Task EnableImplicitSession_WhenTrue_ButInsideTransaction_UsesPooledSession() - { - var cs = ConnectionString + ";EnableImplicitSession=true"; - - await using var conn = new YdbConnection(cs); - await conn.OpenAsync(); - - using var tx = conn.BeginTransaction(); - var cmd = conn.CreateCommand(); - cmd.Transaction = tx; - cmd.CommandText = "SELECT 1"; - var result = Convert.ToInt64(await cmd.ExecuteScalarAsync()); - Assert.Equal(1L, result); - - var pooledSession = conn.GetExecutionSession(useImplicit: false); - var implicitSession = conn.GetExecutionSession(useImplicit: true); - - Assert.Equal(pooledSession, conn.Session); - Assert.NotEqual(pooledSession, implicitSession); - } - - [Fact] - public async Task EnableImplicitSession_WhenFalse_AlwaysUsesPooledSession() - { - var cs = ConnectionString + ";EnableImplicitSession=false"; - - await using var conn = new YdbConnection(cs); - await conn.OpenAsync(); - - var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT CAST(1 AS Int64)"; - var result = (long)(await cmd.ExecuteScalarAsync())!; - Assert.Equal(1L, result); - - var pooledSession = conn.GetExecutionSession(useImplicit: false); - Assert.Equal(pooledSession, conn.Session); - } - - [Fact] - public async Task EnableImplicitSession_DifferentConnectionStrings_HaveDifferentImplicitPools() - { - var cs1 = ConnectionString + ";EnableImplicitSession=true;MinSessionPool=0;DisableDiscovery=false"; - var cs2 = ConnectionString + ";EnableImplicitSession=true;MinSessionPool=1;DisableDiscovery=false"; - - await using var conn1 = new YdbConnection(cs1); - await conn1.OpenAsync(); - var session1 = conn1.GetExecutionSession(useImplicit: true); - - await using var conn2 = new YdbConnection(cs2); - await conn2.OpenAsync(); - var session2 = conn2.GetExecutionSession(useImplicit: true); - - Assert.NotEqual(session1, session2); - } - - [Fact] - public async Task EnableImplicitSession_TwoSequentialCommands_GetDifferentImplicitSessions() - { - var cs = ConnectionString + ";EnableImplicitSession=true"; - await using var conn = new YdbConnection(cs); - await conn.OpenAsync(); - - var s1 = conn.GetExecutionSession(useImplicit: true); - var s2 = conn.GetExecutionSession(useImplicit: true); - - Assert.NotEqual(s1, s2); - } - - [Fact] - public async Task ClearPool_FireAndForget_DoesNotBlock_And_PoolsRecreate() - { - var csBase = - ConnectionString + - ";UseTls=false" + - ";DisableDiscovery=true" + - ";CreateSessionTimeout=3" + - ";ConnectTimeout=3" + - ";KeepAlivePingDelay=0;KeepAlivePingTimeout=0"; - - var csPooled = csBase; // pooled-пул (без флага) - var csImplicit = csBase + ";EnableImplicitSession=true"; // implicit-пул (с флагом) - - // 1) Прогреваем оба пула (pooled и implicit), чтобы они точно были созданы. - await using (var warmPooled = new YdbConnection(csPooled)) - { - await warmPooled.OpenAsync(); - using var cmd = warmPooled.CreateCommand(); - cmd.CommandText = "SELECT 1"; - Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); - } - - await using (var warmImplicit = new YdbConnection(csImplicit)) - { - await warmImplicit.OpenAsync(); - using var cmd = warmImplicit.CreateCommand(); - cmd.CommandText = "SELECT 1"; - Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); - } - - // 2) Вызываем ClearPool для ОБОИХ ключей (по вашей реализации ключи разные). - var clearPooledTask = YdbConnection.ClearPool(new YdbConnection(csPooled)); - var clearImplicitTask = YdbConnection.ClearPool(new YdbConnection(csImplicit)); - - // 3) Убеждаемся, что ClearPool не блокирует — завершается быстро (fail-fast). - // (Если вдруг среда перегружена, можно поднять таймаут до 3–5 секунд.) - var done = await Task.WhenAny(Task.WhenAll(clearPooledTask, clearImplicitTask), Task.Delay(TimeSpan.FromSeconds(2))); - Assert.True(done is not null && done != Task.Delay(TimeSpan.FromSeconds(2)), "ClearPool() must not block."); - - // 4) Проверяем, что пулы корректно пересоздаются после очистки: - // pooled — без флага, implicit — с флагом. - await using (var checkPooled = new YdbConnection(csPooled)) - { - await checkPooled.OpenAsync(); - using var cmd = checkPooled.CreateCommand(); - cmd.CommandText = "SELECT 1"; - Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); - } - - await using (var checkImplicit = new YdbConnection(csImplicit)) - { - await checkImplicit.OpenAsync(); - using var cmd = checkImplicit.CreateCommand(); - cmd.CommandText = "SELECT 1"; - Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); - } - } - - [Fact] - public async Task EnableImplicitSession_ParallelQueries_WorkFine() - { - var cs = ConnectionString + ";EnableImplicitSession=true"; - await using var conn = new YdbConnection(cs); - await conn.OpenAsync(); - - var tasks = Enumerable.Range(0, 16).Select(async _ => - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT 1"; - var v = Convert.ToInt64(await cmd.ExecuteScalarAsync()); - Assert.Equal(1L, v); - }); - await Task.WhenAll(tasks); - } - - [Fact] - public async Task EnableImplicitSession_WithDisableDiscovery_Works() - { - var cs = ConnectionString + ";EnableImplicitSession=true;DisableDiscovery=true"; - await using var conn = new YdbConnection(cs); - await conn.OpenAsync(); - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT 1"; - Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); - } } From 812a07749a8cb7d38295e4aeab3896a8cbf63d1c Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Tue, 12 Aug 2025 19:48:38 +0300 Subject: [PATCH 12/48] feat: add EnableImplicitSession flag support with parsing tests --- .../src/Ado/YdbConnectionStringBuilder.cs | 16 ++++++++++++++++ .../YdbConnectionStringBuilderTests.cs | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs index d135d1ea..fc31e163 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs @@ -41,6 +41,7 @@ private void InitDefaultValues() _maxReceiveMessageSize = GrpcDefaultSettings.MaxReceiveMessageSize; _disableDiscovery = GrpcDefaultSettings.DisableDiscovery; _disableServerBalancer = false; + _enableImplicitSession = false; } public string Host @@ -315,6 +316,18 @@ public int CreateSessionTimeout private int _createSessionTimeout; + public bool EnableImplicitSession + { + get => _enableImplicitSession; + set + { + _enableImplicitSession = value; + SaveValue(nameof(EnableImplicitSession), value); + } + } + + private bool _enableImplicitSession; + public ILoggerFactory? LoggerFactory { get; init; } public ICredentialsProvider? CredentialsProvider { get; init; } @@ -491,6 +504,9 @@ static YdbConnectionOption() AddOption(new YdbConnectionOption(BoolExtractor, (builder, disableServerBalancer) => builder.DisableServerBalancer = disableServerBalancer), "DisableServerBalancer", "Disable Server Balancer"); + AddOption(new YdbConnectionOption(BoolExtractor, + (builder, enableImplicit) => builder.EnableImplicitSession = enableImplicit), + "EnableImplicitSession", "ImplicitSession"); } private static void AddOption(YdbConnectionOption option, params string[] keys) 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 736b1e96..33a9aa15 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs @@ -28,6 +28,7 @@ public void InitDefaultValues_WhenEmptyConstructorInvoke_ReturnDefaultConnection Assert.False(ydbConnectionStringBuilder.DisableDiscovery); Assert.False(ydbConnectionStringBuilder.DisableServerBalancer); Assert.False(ydbConnectionStringBuilder.UseTls); + Assert.False(ydbConnectionStringBuilder.EnableImplicitSession); } [Fact] @@ -50,7 +51,7 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=true;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=true;DisableServerBalancer=true;" + "DisableDiscovery=true;DisableServerBalancer=true;EnableImplicitSession=true;" ); Assert.Equal(2135, connectionString.Port); @@ -74,9 +75,10 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=True;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=True;DisableServerBalancer=True", connectionString.ConnectionString); + "DisableDiscovery=True;DisableServerBalancer=True;EnableImplicitSession=True", connectionString.ConnectionString); Assert.True(connectionString.DisableDiscovery); Assert.True(connectionString.DisableServerBalancer); + Assert.True(connectionString.EnableImplicitSession); } [Fact] From 74eac7d0311f0ed70123fc147135e341bace8383 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 3 Sep 2025 03:53:53 +0300 Subject: [PATCH 13/48] feat: update ImplicitSession for singleton driver --- .../src/Ado/Session/ImplicitSession.cs | 6 +- .../src/Ado/Session/ImplicitSessionSource.cs | 25 +++- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 30 ++++- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 16 +++ .../src/Ado/YdbConnectionStringBuilder.cs | 16 +++ .../test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs | 116 ++++++++++++++++++ .../YdbConnectionStringBuilderTests.cs | 7 +- 7 files changed, 206 insertions(+), 10 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs index 596f78b2..7f80884d 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs @@ -5,9 +5,12 @@ namespace Ydb.Sdk.Ado.Session; internal class ImplicitSession : ISession { - public ImplicitSession(IDriver driver) + private readonly Action? _onClose; + + public ImplicitSession(IDriver driver, Action? onClose = null) { Driver = driver; + _onClose = onClose; } public IDriver Driver { get; } @@ -49,6 +52,7 @@ public void OnNotSuccessStatusCode(StatusCode code) public void Close() { + _onClose?.Invoke(); } private static YdbException NotSupportedTransaction => diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 5adea245..8c09fbfa 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -3,13 +3,30 @@ namespace Ydb.Sdk.Ado.Session; internal sealed class ImplicitSessionSource : ISessionSource { private readonly IDriver _driver; + private readonly Action? _onEmpty; + private int _leased; - internal ImplicitSessionSource(IDriver driver) + internal ImplicitSessionSource(IDriver driver, Action? onEmpty = null) { _driver = driver; + _onEmpty = onEmpty; } - public ValueTask OpenSession(CancellationToken cancellationToken) => new(new ImplicitSession(_driver)); + public ValueTask OpenSession(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + Interlocked.Increment(ref _leased); + + return new ValueTask(new ImplicitSession(_driver, Release)); + } + + private void Release() + { + if (Interlocked.Decrement(ref _leased) == 0) + { + _onEmpty?.Invoke(); + } + } - public async ValueTask DisposeAsync() => await _driver.DisposeAsync(); -} \ No newline at end of file + public ValueTask DisposeAsync() => ValueTask.CompletedTask; +} diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index c8618a54..712a8843 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -216,9 +216,33 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha throw new InvalidOperationException("Transaction mismatched! (Maybe using another connection)"); } - var ydbDataReader = await YdbDataReader.CreateYdbDataReader(await YdbConnection.Session.ExecuteQuery( - preparedSql.ToString(), ydbParameters, execSettings, transaction?.TransactionControl - ), YdbConnection.OnNotSuccessStatusCode, transaction, cancellationToken); + var useImplicit = YdbConnection.EnableImplicitSession && transaction is null; + var session = YdbConnection.GetExecutionSession(useImplicit); + + YdbDataReader ydbDataReader; + try + { + var execResult = await session.ExecuteQuery( + preparedSql.ToString(), + ydbParameters, + execSettings, + transaction?.TransactionControl + ); + + ydbDataReader = await YdbDataReader.CreateYdbDataReader( + execResult, + YdbConnection.OnNotSuccessStatusCode, + transaction, + cancellationToken + ); + } + finally + { + if (useImplicit) + { + session.Close(); + } + } YdbConnection.LastReader = ydbDataReader; YdbConnection.LastCommand = CommandText; diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 1dd39ef1..eae43556 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -39,6 +39,21 @@ internal ISession Session private ISession _session = null!; + private ImplicitSessionSource? _implicitSessionSource; + + internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; + + internal ISession GetExecutionSession(bool useImplicit) + { + ThrowIfConnectionClosed(); + + if (!useImplicit) + return Session; + + _implicitSessionSource ??= new ImplicitSessionSource(Session.Driver, onEmpty: () => _implicitSessionSource = null); + return _implicitSessionSource.OpenSession(CancellationToken.None).GetAwaiter().GetResult(); + } + public YdbConnection() { } @@ -127,6 +142,7 @@ public override async Task CloseAsync() finally { _session.Close(); + _implicitSessionSource = null; } } diff --git a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs index d5ce7f51..367764ac 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs @@ -40,6 +40,7 @@ private void InitDefaultValues() _maxReceiveMessageSize = GrpcDefaultSettings.MaxReceiveMessageSize; _disableDiscovery = GrpcDefaultSettings.DisableDiscovery; _disableServerBalancer = false; + _enableImplicitSession = false; } public string Host @@ -314,6 +315,18 @@ public int CreateSessionTimeout private int _createSessionTimeout; + public bool EnableImplicitSession + { + get => _enableImplicitSession; + set + { + _enableImplicitSession = value; + SaveValue(nameof(EnableImplicitSession), value); + } + } + + private bool _enableImplicitSession; + public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance; public ICredentialsProvider? CredentialsProvider { get; init; } @@ -495,6 +508,9 @@ static YdbConnectionOption() AddOption(new YdbConnectionOption(BoolExtractor, (builder, disableServerBalancer) => builder.DisableServerBalancer = disableServerBalancer), "DisableServerBalancer", "Disable Server Balancer"); + AddOption(new YdbConnectionOption(BoolExtractor, + (builder, enableImplicitSession) => builder.EnableImplicitSession = enableImplicitSession), + "EnableImplicitSession", "ImplicitSession"); } private static void AddOption(YdbConnectionOption option, params string[] keys) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs index e322da34..bc491a72 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs @@ -204,6 +204,122 @@ public async Task ExecuteScalar_WhenSelectNoRows_ReturnNull() .ExecuteScalarAsync()); } + [Fact] + public async Task ImplicitSession_SimpleScalar_Works() + { + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT 40 + 2;"; + var scalar = await cmd.ExecuteScalarAsync(); + Assert.Equal(42, Convert.ToInt32(scalar)); + } + + [Fact] + public async Task ImplicitSession_RepeatedScalars_WorksManyTimes() + { + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + for (var i = 0; i < 30; i++) + { + var cmd = connection.CreateCommand(); + cmd.CommandText = $"SELECT {i};"; + var scalar = await cmd.ExecuteScalarAsync(); + Assert.Equal(i, Convert.ToInt32(scalar)); + } + } + + [Fact] + public void ImplicitSession_ConcurrentCommand_IsStillBlockedByBusyCheck() + { + using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT 1; SELECT 1;"; + using var reader = cmd.ExecuteReader(); + + var ex = Assert.Throws(() => cmd.ExecuteReader()); + Assert.Equal("A command is already in progress: SELECT 1; SELECT 1;", ex.Message); + } + + [Fact] + public async Task ImplicitSession_WithExplicitTransaction_UsesExplicitSessionAndCommits() + { + var table = $"Implicit_{Guid.NewGuid():N}"; + + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + try + { + await using (var create = connection.CreateCommand()) + { + create.CommandText = $""" + CREATE TABLE {table} ( + Id Int32, + Name Text, + PRIMARY KEY (Id) + ) + """; + await create.ExecuteNonQueryAsync(); + } + + var tx = connection.BeginTransaction(); + await using (var insert = connection.CreateCommand()) + { + insert.Transaction = tx; + insert.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (1, 'A');"; + await insert.ExecuteNonQueryAsync(); + insert.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (2, 'B');"; + await insert.ExecuteNonQueryAsync(); + } + + await tx.CommitAsync(); + + await using (var check = connection.CreateCommand()) + { + check.CommandText = $"SELECT COUNT(*) FROM {table};"; + var count = Convert.ToInt32(await check.ExecuteScalarAsync()); + Assert.Equal(2, count); + } + } + finally + { + await using var drop = connection.CreateCommand(); + drop.CommandText = $"DROP TABLE {table}"; + await drop.ExecuteNonQueryAsync(); + } + } + + [Fact] + public async Task ImplicitSession_Cancellation_AfterFirstResult_StillReturnsFirst() + { + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + var cmd = new YdbCommand(connection) { CommandText = "SELECT 1; SELECT 1;" }; + using var cts = new CancellationTokenSource(); + + var reader = await cmd.ExecuteReaderAsync(cts.Token); + + await reader.ReadAsync(cts.Token); + Assert.Equal(1, reader.GetValue(0)); + Assert.True(await reader.NextResultAsync(cts.Token)); + + await cts.CancelAsync(); + + await reader.ReadAsync(cts.Token); + Assert.Equal(1, reader.GetValue(0)); + Assert.False(await reader.NextResultAsync()); + } public class Data(DbType dbType, T expected, bool isNullable = false) { 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 87f02ea3..8a7aa70a 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionStringBuilderTests.cs @@ -30,6 +30,7 @@ public void InitDefaultValues_WhenEmptyConstructorInvoke_ReturnDefaultConnection Assert.False(ydbConnectionStringBuilder.DisableDiscovery); Assert.False(ydbConnectionStringBuilder.DisableServerBalancer); Assert.False(ydbConnectionStringBuilder.UseTls); + Assert.False(ydbConnectionStringBuilder.EnableImplicitSession); Assert.Equal("UseTls=False;Host=localhost;Port=2136;Database=/local;User=;Password=;ConnectTimeout=5;" + "KeepAlivePingDelay=10;KeepAlivePingTimeout=10;EnableMultipleHttp2Connections=False;" + @@ -54,7 +55,7 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "Host=server;Port=2135;Database=/my/path;User=Kirill;UseTls=true;MinSessionPool=10;MaxSessionPool=50;" + "CreateSessionTimeout=30;SessionIdleTimeout=600;ConnectTimeout=30;KeepAlivePingDelay=30;" + "KeepAlivePingTimeout=60;EnableMultipleHttp2Connections=true;MaxSendMessageSize=1000000;" + - "MaxReceiveMessageSize=1000000;DisableDiscovery=true;DisableServerBalancer=true;" + "MaxReceiveMessageSize=1000000;DisableDiscovery=true;DisableServerBalancer=true;EnableImplicitSession=true;" ); Assert.Equal(2135, ydbConnectionStringBuilder.Port); @@ -78,9 +79,11 @@ public void InitConnectionStringBuilder_WhenExpectedKeys_ReturnUpdatedConnection "ConnectTimeout=30;KeepAlivePingDelay=30;KeepAlivePingTimeout=60;" + "EnableMultipleHttp2Connections=True;" + "MaxSendMessageSize=1000000;MaxReceiveMessageSize=1000000;" + - "DisableDiscovery=True;DisableServerBalancer=True", ydbConnectionStringBuilder.ConnectionString); + "DisableDiscovery=True;DisableServerBalancer=True;EnableImplicitSession=True", + ydbConnectionStringBuilder.ConnectionString); Assert.True(ydbConnectionStringBuilder.DisableDiscovery); Assert.True(ydbConnectionStringBuilder.DisableServerBalancer); + 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", From c57c3d62482e9ce1bd927d6aed4b904ac37a7a27 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 3 Sep 2025 05:40:00 +0300 Subject: [PATCH 14/48] try fix DisableParallelization in PMTests and autoformat --- src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs | 5 +---- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 3 ++- src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs | 4 +++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs index 7f80884d..76a0691d 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs @@ -50,10 +50,7 @@ public void OnNotSuccessStatusCode(StatusCode code) { } - public void Close() - { - _onClose?.Invoke(); - } + public void Close() => _onClose?.Invoke(); private static YdbException NotSupportedTransaction => new(StatusCode.BadRequest, "Transactions are not supported in implicit sessions"); diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index eae43556..17a00eff 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -50,7 +50,8 @@ internal ISession GetExecutionSession(bool useImplicit) if (!useImplicit) return Session; - _implicitSessionSource ??= new ImplicitSessionSource(Session.Driver, onEmpty: () => _implicitSessionSource = null); + _implicitSessionSource ??= + new ImplicitSessionSource(Session.Driver, onEmpty: () => _implicitSessionSource = null); return _implicitSessionSource.OpenSession(CancellationToken.None).GetAwaiter().GetResult(); } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs index 562f3b04..89fdc793 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs @@ -3,8 +3,10 @@ namespace Ydb.Sdk.Ado.Tests; -[Collection("PoolManagerTests")] [CollectionDefinition("PoolManagerTests", DisableParallelization = true)] +public sealed class PoolManagerCollection { } + +[Collection("PoolManagerTests")] public class PoolManagerTests { [Theory] From a5154b52621dc913d495f39cb6f33612aad94d43 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 3 Sep 2025 06:04:55 +0300 Subject: [PATCH 15/48] fix lint --- src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs index 89fdc793..f3a940b4 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs @@ -4,7 +4,7 @@ namespace Ydb.Sdk.Ado.Tests; [CollectionDefinition("PoolManagerTests", DisableParallelization = true)] -public sealed class PoolManagerCollection { } +public sealed class PoolManagerCollection; [Collection("PoolManagerTests")] public class PoolManagerTests From 3addeae6cc0b2d3fc0eaa90595702cea10f1b327 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 3 Sep 2025 22:37:50 +0300 Subject: [PATCH 16/48] fix: keep single command session; make ImplicitSessionSource dispose-safe --- .../src/Ado/Session/ImplicitSessionSource.cs | 23 ++++++++++- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 38 ++++++------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 8c09fbfa..83a09beb 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -5,6 +5,7 @@ internal sealed class ImplicitSessionSource : ISessionSource private readonly IDriver _driver; private readonly Action? _onEmpty; private int _leased; + private int _closed; internal ImplicitSessionSource(IDriver driver, Action? onEmpty = null) { @@ -15,8 +16,18 @@ internal ImplicitSessionSource(IDriver driver, Action? onEmpty = null) public ValueTask OpenSession(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); + + if (Volatile.Read(ref _closed) == 1) + throw new ObjectDisposedException(nameof(ImplicitSessionSource)); + Interlocked.Increment(ref _leased); + if (Volatile.Read(ref _closed) == 1) + { + Interlocked.Decrement(ref _leased); + throw new ObjectDisposedException(nameof(ImplicitSessionSource)); + } + return new ValueTask(new ImplicitSession(_driver, Release)); } @@ -28,5 +39,15 @@ private void Release() } } - public ValueTask DisposeAsync() => ValueTask.CompletedTask; + public ValueTask DisposeAsync() + { + Interlocked.Exchange(ref _closed, 1); + + if (Volatile.Read(ref _leased) == 0) + { + _onEmpty?.Invoke(); + } + + return ValueTask.CompletedTask; + } } diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 712a8843..085c7509 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -216,33 +216,19 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha throw new InvalidOperationException("Transaction mismatched! (Maybe using another connection)"); } - var useImplicit = YdbConnection.EnableImplicitSession && transaction is null; - var session = YdbConnection.GetExecutionSession(useImplicit); + var execResult = await YdbConnection.Session.ExecuteQuery( + preparedSql.ToString(), + ydbParameters, + execSettings, + transaction?.TransactionControl + ); - YdbDataReader ydbDataReader; - try - { - var execResult = await session.ExecuteQuery( - preparedSql.ToString(), - ydbParameters, - execSettings, - transaction?.TransactionControl - ); - - ydbDataReader = await YdbDataReader.CreateYdbDataReader( - execResult, - YdbConnection.OnNotSuccessStatusCode, - transaction, - cancellationToken - ); - } - finally - { - if (useImplicit) - { - session.Close(); - } - } + var ydbDataReader = await YdbDataReader.CreateYdbDataReader( + execResult, + YdbConnection.OnNotSuccessStatusCode, + transaction, + cancellationToken + ); YdbConnection.LastReader = ydbDataReader; YdbConnection.LastCommand = CommandText; From 0c53efbefe21b5633ac8853788fa1106b94bbc6e Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Thu, 4 Sep 2025 16:47:11 +0300 Subject: [PATCH 17/48] Move implicit session creation to PoolManager by flag --- src/Ydb.Sdk/src/Ado/PoolManager.cs | 18 ++++++-- .../src/Ado/Session/ImplicitSessionSource.cs | 42 +++++++++++-------- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 19 +-------- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index 1eae6b04..2e737f8b 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -33,10 +33,22 @@ CancellationToken cancellationToken !cacheDriver.IsDisposed ? cacheDriver : Drivers[settings.GrpcConnectionString] = await settings.BuildDriver(); - driver.RegisterOwner(); - var factory = new PoolingSessionFactory(driver, settings); - var newSessionPool = new PoolingSessionSource(factory, settings); + ISessionSource newSessionPool; + if (settings.EnableImplicitSession) + { + var key = settings.ConnectionString; + newSessionPool = new ImplicitSessionSource( + driver, + onEmpty: () => Pools.TryRemove(key, out _) + ); + } + else + { + driver.RegisterOwner(); + var factory = new PoolingSessionFactory(driver, settings); + newSessionPool = new PoolingSessionSource(factory, settings); + } Pools[settings.ConnectionString] = newSessionPool; diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 83a09beb..71bf29af 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -3,49 +3,57 @@ namespace Ydb.Sdk.Ado.Session; internal sealed class ImplicitSessionSource : ISessionSource { private readonly IDriver _driver; - private readonly Action? _onEmpty; - private int _leased; - private int _closed; + private readonly Action? _onBecameEmpty; + private int _isDisposed; + private int _activeLeaseCount; internal ImplicitSessionSource(IDriver driver, Action? onEmpty = null) { - _driver = driver; - _onEmpty = onEmpty; + _driver = driver ?? throw new ArgumentNullException(nameof(driver)); + _onBecameEmpty = onEmpty; } public ValueTask OpenSession(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (Volatile.Read(ref _closed) == 1) + if (!TryAcquireLease()) throw new ObjectDisposedException(nameof(ImplicitSessionSource)); - Interlocked.Increment(ref _leased); + return new ValueTask(new ImplicitSession(_driver, ReleaseLease)); + } + + private bool TryAcquireLease() + { + if (Volatile.Read(ref _isDisposed) != 0) + return false; - if (Volatile.Read(ref _closed) == 1) + Interlocked.Increment(ref _activeLeaseCount); + + if (Volatile.Read(ref _isDisposed) != 0) { - Interlocked.Decrement(ref _leased); - throw new ObjectDisposedException(nameof(ImplicitSessionSource)); + Interlocked.Decrement(ref _activeLeaseCount); + return false; } - return new ValueTask(new ImplicitSession(_driver, Release)); + return true; } - private void Release() + private void ReleaseLease() { - if (Interlocked.Decrement(ref _leased) == 0) + if (Interlocked.Decrement(ref _activeLeaseCount) == 0) { - _onEmpty?.Invoke(); + _onBecameEmpty?.Invoke(); } } public ValueTask DisposeAsync() { - Interlocked.Exchange(ref _closed, 1); + Interlocked.Exchange(ref _isDisposed, 1); - if (Volatile.Read(ref _leased) == 0) + if (Volatile.Read(ref _activeLeaseCount) == 0) { - _onEmpty?.Invoke(); + _onBecameEmpty?.Invoke(); } return ValueTask.CompletedTask; diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 17a00eff..18ebc331 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -39,22 +39,6 @@ internal ISession Session private ISession _session = null!; - private ImplicitSessionSource? _implicitSessionSource; - - internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; - - internal ISession GetExecutionSession(bool useImplicit) - { - ThrowIfConnectionClosed(); - - if (!useImplicit) - return Session; - - _implicitSessionSource ??= - new ImplicitSessionSource(Session.Driver, onEmpty: () => _implicitSessionSource = null); - return _implicitSessionSource.OpenSession(CancellationToken.None).GetAwaiter().GetResult(); - } - public YdbConnection() { } @@ -143,7 +127,6 @@ public override async Task CloseAsync() finally { _session.Close(); - _implicitSessionSource = null; } } @@ -165,7 +148,7 @@ public override string ConnectionString public override ConnectionState State => ConnectionState; - private ConnectionState ConnectionState { get; set; } = ConnectionState.Closed; // Invoke AsyncOpen() + private ConnectionState ConnectionState { get; set; } = ConnectionState.Closed; internal void OnNotSuccessStatusCode(StatusCode code) { From 46b9ca381abf6afeb54515b2980013de008e8f69 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Thu, 4 Sep 2025 19:47:52 +0300 Subject: [PATCH 18/48] test: validate implicit session disallows transactions but supports non-transactional commands --- .../test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs index bc491a72..e70af8bf 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs @@ -249,7 +249,7 @@ public void ImplicitSession_ConcurrentCommand_IsStillBlockedByBusyCheck() } [Fact] - public async Task ImplicitSession_WithExplicitTransaction_UsesExplicitSessionAndCommits() + public async Task ImplicitSession_DisallowsTransactions_And_AllowsNonTransactionalCommands() { var table = $"Implicit_{Guid.NewGuid():N}"; @@ -271,23 +271,27 @@ PRIMARY KEY (Id) await create.ExecuteNonQueryAsync(); } - var tx = connection.BeginTransaction(); await using (var insert = connection.CreateCommand()) { - insert.Transaction = tx; insert.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (1, 'A');"; await insert.ExecuteNonQueryAsync(); - insert.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (2, 'B');"; - await insert.ExecuteNonQueryAsync(); } - await tx.CommitAsync(); + var tx = connection.BeginTransaction(); + await using (var insertTx = connection.CreateCommand()) + { + insertTx.Transaction = tx; + insertTx.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (2, 'B');"; + var ex = await Assert.ThrowsAsync(async () => await insertTx.ExecuteNonQueryAsync()); + Assert.Contains("Transactions are not supported in implicit sessions", ex.Message); + } + await tx.RollbackAsync(); await using (var check = connection.CreateCommand()) { check.CommandText = $"SELECT COUNT(*) FROM {table};"; var count = Convert.ToInt32(await check.ExecuteScalarAsync()); - Assert.Equal(2, count); + Assert.Equal(1, count); } } finally From c6d6a5935c32cb14ad5bd4a1d497c083d0d571cb Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Thu, 4 Sep 2025 23:45:27 +0300 Subject: [PATCH 19/48] fix lint --- src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs index e70af8bf..5357d8fa 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs @@ -285,6 +285,7 @@ PRIMARY KEY (Id) var ex = await Assert.ThrowsAsync(async () => await insertTx.ExecuteNonQueryAsync()); Assert.Contains("Transactions are not supported in implicit sessions", ex.Message); } + await tx.RollbackAsync(); await using (var check = connection.CreateCommand()) From 741e28b23e904ceb4ab2d3abac589d6438701629 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 15:27:15 +0300 Subject: [PATCH 20/48] feat(ado): add ImplicitSession with PoolManager integration and stress-tested lifecycle --- .../src/Ado/Session/ImplicitSession.cs | 10 +- .../src/Ado/Session/ImplicitSessionSource.cs | 18 +-- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 16 +- .../Session/YdbImplicitStressTests.cs | 144 ++++++++++++++++++ .../Session/YdbImplictConnectionTests.cs | 128 ++++++++++++++++ .../test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs | 121 --------------- 6 files changed, 288 insertions(+), 149 deletions(-) create mode 100644 src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs create mode 100644 src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplictConnectionTests.cs diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs index 76a0691d..1ff298be 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs @@ -5,12 +5,12 @@ namespace Ydb.Sdk.Ado.Session; internal class ImplicitSession : ISession { - private readonly Action? _onClose; + private readonly ImplicitSessionSource _source; - public ImplicitSession(IDriver driver, Action? onClose = null) + public ImplicitSession(IDriver driver, ImplicitSessionSource source) { - Driver = driver; - _onClose = onClose; + Driver = driver ?? throw new ArgumentNullException(nameof(driver)); + _source = source ?? throw new ArgumentNullException(nameof(source)); } public IDriver Driver { get; } @@ -50,7 +50,7 @@ public void OnNotSuccessStatusCode(StatusCode code) { } - public void Close() => _onClose?.Invoke(); + public void Close() => _source.ReleaseLease(); private static YdbException NotSupportedTransaction => new(StatusCode.BadRequest, "Transactions are not supported in implicit sessions"); diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 71bf29af..0086f4e4 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -20,7 +20,7 @@ public ValueTask OpenSession(CancellationToken cancellationToken) if (!TryAcquireLease()) throw new ObjectDisposedException(nameof(ImplicitSessionSource)); - return new ValueTask(new ImplicitSession(_driver, ReleaseLease)); + return new ValueTask(new ImplicitSession(_driver, this)); } private bool TryAcquireLease() @@ -39,23 +39,21 @@ private bool TryAcquireLease() return true; } - private void ReleaseLease() - { - if (Interlocked.Decrement(ref _activeLeaseCount) == 0) - { - _onBecameEmpty?.Invoke(); - } - } + internal void ReleaseLease() => Interlocked.Decrement(ref _activeLeaseCount); public ValueTask DisposeAsync() { Interlocked.Exchange(ref _isDisposed, 1); - if (Volatile.Read(ref _activeLeaseCount) == 0) + var spinner = new SpinWait(); + while (Volatile.Read(ref _activeLeaseCount) != 0) { - _onBecameEmpty?.Invoke(); + spinner.SpinOnce(); } + _onBecameEmpty?.Invoke(); + return ValueTask.CompletedTask; } + } diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 085c7509..f7f1233e 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -216,19 +216,9 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha throw new InvalidOperationException("Transaction mismatched! (Maybe using another connection)"); } - var execResult = await YdbConnection.Session.ExecuteQuery( - preparedSql.ToString(), - ydbParameters, - execSettings, - transaction?.TransactionControl - ); - - var ydbDataReader = await YdbDataReader.CreateYdbDataReader( - execResult, - YdbConnection.OnNotSuccessStatusCode, - transaction, - cancellationToken - ); + var ydbDataReader = await YdbDataReader.CreateYdbDataReader(await YdbConnection.Session.ExecuteQuery( + preparedSql.ToString(), ydbParameters, execSettings, transaction?.TransactionControl + ), YdbConnection.OnNotSuccessStatusCode, transaction, cancellationToken); YdbConnection.LastReader = ydbDataReader; YdbConnection.LastCommand = CommandText; diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs new file mode 100644 index 00000000..9942a8b0 --- /dev/null +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs @@ -0,0 +1,144 @@ +using Moq; +using Xunit; +using Ydb.Sdk.Ado.Session; + +namespace Ydb.Sdk.Ado.Tests.Session; + +public class YdbImplicitStressTests : TestBase +{ + private static IDriver DummyDriver() => new Mock(MockBehavior.Strict).Object; + + [Fact(Timeout = 30_000)] + public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() + { + var driver = DummyDriver(); + + var onEmptyCalls = 0; + var opened = 0; + var closed = 0; + + var source = new ImplicitSessionSource(driver, onEmpty: () => Interlocked.Increment(ref onEmptyCalls)); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + var workers = Enumerable.Range(0, 200).Select(async i => + { + var rnd = new Random(unchecked(i ^ Environment.TickCount)); + for (var j = 0; j < 10; j++) + { + try + { + var s = await source.OpenSession(cts.Token); + Interlocked.Increment(ref opened); + + await Task.Delay(rnd.Next(0, 5), cts.Token); + + s.Close(); + Interlocked.Increment(ref closed); + } + catch (ObjectDisposedException) + { + } + } + }).ToArray(); + + var disposer = Task.Run(async () => + { + await Task.Delay(10, cts.Token); + await source.DisposeAsync(); + }, cts.Token); + + await Task.WhenAll(workers.Append(disposer)); + + Assert.True(opened > 0); + Assert.Equal(opened, closed); + Assert.Equal(1, Volatile.Read(ref onEmptyCalls)); + + await Assert.ThrowsAsync( + () => source.OpenSession(CancellationToken.None).AsTask()); + } + + [Fact(Timeout = 30_000)] + public async Task Stress_Counts_AreBalanced() + { + var driver = DummyDriver(); + + var opened = 0; + var closed = 0; + var onEmptyCalls = 0; + + var source = new ImplicitSessionSource(driver, onEmpty: () => Interlocked.Increment(ref onEmptyCalls)); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + var workers = Enumerable.Range(0, 200).Select(async i => + { + var rnd = new Random(unchecked(i ^ Environment.TickCount)); + for (var j = 0; j < 10; j++) + { + try + { + var s = await source.OpenSession(cts.Token); + Interlocked.Increment(ref opened); + + await Task.Delay(rnd.Next(0, 3), cts.Token); + + s.Close(); + Interlocked.Increment(ref closed); + } + catch (ObjectDisposedException) + { + } + } + }).ToArray(); + + var disposer = Task.Run(async () => await source.DisposeAsync(), cts.Token); + + await Task.WhenAll(workers.Append(disposer)); + + Assert.Equal(opened, closed); + Assert.Equal(1, onEmptyCalls); + Assert.True(opened > 0); + + await Assert.ThrowsAsync( + () => source.OpenSession(CancellationToken.None).AsTask()); + } + + [Fact(Timeout = 30_000)] + public async Task Open_RacingWithDispose_StateRemainsConsistent() + { + var driver = DummyDriver(); + + var onEmptyCalls = 0; + var source = new ImplicitSessionSource(driver, onEmpty: () => Interlocked.Increment(ref onEmptyCalls)); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + var opens = Enumerable.Range(0, 1000).Select(async _ => + { + try + { + var s = await source.OpenSession(cts.Token); + s.Close(); + return 1; + } + catch (ObjectDisposedException) + { + return 0; + } + }).ToArray(); + + var disposeTask = Task.Run(async () => + { + await Task.Yield(); + await source.DisposeAsync(); + }, cts.Token); + + await Task.WhenAll(opens.Append(disposeTask)); + + Assert.Equal(1, Volatile.Read(ref onEmptyCalls)); + + await Assert.ThrowsAsync( + () => source.OpenSession(CancellationToken.None).AsTask()); + } +} diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplictConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplictConnectionTests.cs new file mode 100644 index 00000000..6f3124f6 --- /dev/null +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplictConnectionTests.cs @@ -0,0 +1,128 @@ +using Xunit; + +namespace Ydb.Sdk.Ado.Tests.Session; + +public class YdbImplictConnectionTests : TestBase +{ + [Fact] + public async Task ImplicitSession_SimpleScalar_Works() + { + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT 40 + 2;"; + var scalar = await cmd.ExecuteScalarAsync(); + Assert.Equal(42, Convert.ToInt32(scalar)); + } + + [Fact] + public async Task ImplicitSession_RepeatedScalars_WorksManyTimes() + { + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + for (var i = 0; i < 30; i++) + { + var cmd = connection.CreateCommand(); + cmd.CommandText = $"SELECT {i};"; + var scalar = await cmd.ExecuteScalarAsync(); + Assert.Equal(i, Convert.ToInt32(scalar)); + } + } + + [Fact] + public void ImplicitSession_ConcurrentCommand_IsStillBlockedByBusyCheck() + { + using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT 1; SELECT 1;"; + using var reader = cmd.ExecuteReader(); + + var ex = Assert.Throws(() => cmd.ExecuteReader()); + Assert.Equal("A command is already in progress: SELECT 1; SELECT 1;", ex.Message); + } + + [Fact] + public async Task ImplicitSession_DisallowsTransactions_And_AllowsNonTransactionalCommands() + { + var table = $"Implicit_{Guid.NewGuid():N}"; + + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + try + { + await using (var create = connection.CreateCommand()) + { + create.CommandText = $""" + CREATE TABLE {table} ( + Id Int32, + Name Text, + PRIMARY KEY (Id) + ) + """; + await create.ExecuteNonQueryAsync(); + } + + await using (var insert = connection.CreateCommand()) + { + insert.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (1, 'A');"; + await insert.ExecuteNonQueryAsync(); + } + + var tx = connection.BeginTransaction(); + await using (var insertTx = connection.CreateCommand()) + { + insertTx.Transaction = tx; + insertTx.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (2, 'B');"; + var ex = await Assert.ThrowsAsync(async () => await insertTx.ExecuteNonQueryAsync()); + Assert.Contains("Transactions are not supported in implicit sessions", ex.Message); + } + + await tx.RollbackAsync(); + + await using (var check = connection.CreateCommand()) + { + check.CommandText = $"SELECT COUNT(*) FROM {table};"; + var count = Convert.ToInt32(await check.ExecuteScalarAsync()); + Assert.Equal(1, count); + } + } + finally + { + await using var drop = connection.CreateCommand(); + drop.CommandText = $"DROP TABLE {table}"; + await drop.ExecuteNonQueryAsync(); + } + } + + [Fact] + public async Task ImplicitSession_Cancellation_AfterFirstResult_StillReturnsFirst() + { + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + var cmd = new YdbCommand(connection) { CommandText = "SELECT 1; SELECT 1;" }; + using var cts = new CancellationTokenSource(); + + var reader = await cmd.ExecuteReaderAsync(cts.Token); + + await reader.ReadAsync(cts.Token); + Assert.Equal(1, reader.GetValue(0)); + Assert.True(await reader.NextResultAsync(cts.Token)); + + await cts.CancelAsync(); + + await reader.ReadAsync(cts.Token); + Assert.Equal(1, reader.GetValue(0)); + Assert.False(await reader.NextResultAsync()); + } +} diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs index 5357d8fa..e322da34 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs @@ -204,127 +204,6 @@ public async Task ExecuteScalar_WhenSelectNoRows_ReturnNull() .ExecuteScalarAsync()); } - [Fact] - public async Task ImplicitSession_SimpleScalar_Works() - { - await using var connection = CreateConnection(); - connection.ConnectionString += ";EnableImplicitSession=true"; - await connection.OpenAsync(); - - var cmd = connection.CreateCommand(); - cmd.CommandText = "SELECT 40 + 2;"; - var scalar = await cmd.ExecuteScalarAsync(); - Assert.Equal(42, Convert.ToInt32(scalar)); - } - - [Fact] - public async Task ImplicitSession_RepeatedScalars_WorksManyTimes() - { - await using var connection = CreateConnection(); - connection.ConnectionString += ";EnableImplicitSession=true"; - await connection.OpenAsync(); - - for (var i = 0; i < 30; i++) - { - var cmd = connection.CreateCommand(); - cmd.CommandText = $"SELECT {i};"; - var scalar = await cmd.ExecuteScalarAsync(); - Assert.Equal(i, Convert.ToInt32(scalar)); - } - } - - [Fact] - public void ImplicitSession_ConcurrentCommand_IsStillBlockedByBusyCheck() - { - using var connection = CreateConnection(); - connection.ConnectionString += ";EnableImplicitSession=true"; - connection.Open(); - - var cmd = connection.CreateCommand(); - cmd.CommandText = "SELECT 1; SELECT 1;"; - using var reader = cmd.ExecuteReader(); - - var ex = Assert.Throws(() => cmd.ExecuteReader()); - Assert.Equal("A command is already in progress: SELECT 1; SELECT 1;", ex.Message); - } - - [Fact] - public async Task ImplicitSession_DisallowsTransactions_And_AllowsNonTransactionalCommands() - { - var table = $"Implicit_{Guid.NewGuid():N}"; - - await using var connection = CreateConnection(); - connection.ConnectionString += ";EnableImplicitSession=true"; - await connection.OpenAsync(); - - try - { - await using (var create = connection.CreateCommand()) - { - create.CommandText = $""" - CREATE TABLE {table} ( - Id Int32, - Name Text, - PRIMARY KEY (Id) - ) - """; - await create.ExecuteNonQueryAsync(); - } - - await using (var insert = connection.CreateCommand()) - { - insert.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (1, 'A');"; - await insert.ExecuteNonQueryAsync(); - } - - var tx = connection.BeginTransaction(); - await using (var insertTx = connection.CreateCommand()) - { - insertTx.Transaction = tx; - insertTx.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (2, 'B');"; - var ex = await Assert.ThrowsAsync(async () => await insertTx.ExecuteNonQueryAsync()); - Assert.Contains("Transactions are not supported in implicit sessions", ex.Message); - } - - await tx.RollbackAsync(); - - await using (var check = connection.CreateCommand()) - { - check.CommandText = $"SELECT COUNT(*) FROM {table};"; - var count = Convert.ToInt32(await check.ExecuteScalarAsync()); - Assert.Equal(1, count); - } - } - finally - { - await using var drop = connection.CreateCommand(); - drop.CommandText = $"DROP TABLE {table}"; - await drop.ExecuteNonQueryAsync(); - } - } - - [Fact] - public async Task ImplicitSession_Cancellation_AfterFirstResult_StillReturnsFirst() - { - await using var connection = CreateConnection(); - connection.ConnectionString += ";EnableImplicitSession=true"; - await connection.OpenAsync(); - - var cmd = new YdbCommand(connection) { CommandText = "SELECT 1; SELECT 1;" }; - using var cts = new CancellationTokenSource(); - - var reader = await cmd.ExecuteReaderAsync(cts.Token); - - await reader.ReadAsync(cts.Token); - Assert.Equal(1, reader.GetValue(0)); - Assert.True(await reader.NextResultAsync(cts.Token)); - - await cts.CancelAsync(); - - await reader.ReadAsync(cts.Token); - Assert.Equal(1, reader.GetValue(0)); - Assert.False(await reader.NextResultAsync()); - } public class Data(DbType dbType, T expected, bool isNullable = false) { From 09dd8958e048d97dc3b6a86af9d157ffb6d3ac6e Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 16:02:42 +0300 Subject: [PATCH 21/48] fix lint --- .../src/Ado/Session/ImplicitSessionSource.cs | 1 - src/Ydb.Sdk/src/Ado/YdbCommand.cs | 4 +- .../Session/YdbImplicitStressTests.cs | 84 ++++++++++--------- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 0086f4e4..90fee01e 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -55,5 +55,4 @@ public ValueTask DisposeAsync() return ValueTask.CompletedTask; } - } diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index f7f1233e..c8618a54 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -217,8 +217,8 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha } var ydbDataReader = await YdbDataReader.CreateYdbDataReader(await YdbConnection.Session.ExecuteQuery( - preparedSql.ToString(), ydbParameters, execSettings, transaction?.TransactionControl - ), YdbConnection.OnNotSuccessStatusCode, transaction, cancellationToken); + preparedSql.ToString(), ydbParameters, execSettings, transaction?.TransactionControl + ), YdbConnection.OnNotSuccessStatusCode, transaction, cancellationToken); YdbConnection.LastReader = ydbDataReader; YdbConnection.LastCommand = CommandText; diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs index 9942a8b0..23cda97b 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs @@ -8,33 +8,40 @@ public class YdbImplicitStressTests : TestBase { private static IDriver DummyDriver() => new Mock(MockBehavior.Strict).Object; + private sealed class Counter + { + public int Value; + public void Inc() => Interlocked.Increment(ref Value); + } + [Fact(Timeout = 30_000)] public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() { var driver = DummyDriver(); - var onEmptyCalls = 0; - var opened = 0; - var closed = 0; + var onEmpty = new Counter(); + var opened = new Counter(); + var closed = new Counter(); - var source = new ImplicitSessionSource(driver, onEmpty: () => Interlocked.Increment(ref onEmptyCalls)); + var source = new ImplicitSessionSource(driver, onEmpty: onEmpty.Inc); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var token = cts.Token; var workers = Enumerable.Range(0, 200).Select(async i => { - var rnd = new Random(unchecked(i ^ Environment.TickCount)); + var rnd = new Random(i ^ Environment.TickCount); for (var j = 0; j < 10; j++) { try { - var s = await source.OpenSession(cts.Token); - Interlocked.Increment(ref opened); + var s = await source.OpenSession(token); + opened.Inc(); - await Task.Delay(rnd.Next(0, 5), cts.Token); + await Task.Delay(rnd.Next(0, 5), token); s.Close(); - Interlocked.Increment(ref closed); + closed.Inc(); } catch (ObjectDisposedException) { @@ -44,47 +51,47 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() var disposer = Task.Run(async () => { - await Task.Delay(10, cts.Token); + await Task.Delay(10, token); await source.DisposeAsync(); - }, cts.Token); + }, token); await Task.WhenAll(workers.Append(disposer)); - Assert.True(opened > 0); - Assert.Equal(opened, closed); - Assert.Equal(1, Volatile.Read(ref onEmptyCalls)); + Assert.True(opened.Value > 0); + Assert.Equal(opened.Value, closed.Value); + Assert.Equal(1, Volatile.Read(ref onEmpty.Value)); - await Assert.ThrowsAsync( - () => source.OpenSession(CancellationToken.None).AsTask()); + await Assert.ThrowsAsync(() => source.OpenSession(CancellationToken.None).AsTask()); } - + [Fact(Timeout = 30_000)] public async Task Stress_Counts_AreBalanced() { var driver = DummyDriver(); - var opened = 0; - var closed = 0; - var onEmptyCalls = 0; + var opened = new Counter(); + var closed = new Counter(); + var onEmpty = new Counter(); - var source = new ImplicitSessionSource(driver, onEmpty: () => Interlocked.Increment(ref onEmptyCalls)); + var source = new ImplicitSessionSource(driver, onEmpty: onEmpty.Inc); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var token = cts.Token; var workers = Enumerable.Range(0, 200).Select(async i => { - var rnd = new Random(unchecked(i ^ Environment.TickCount)); + var rnd = new Random(i ^ Environment.TickCount); for (var j = 0; j < 10; j++) { try { - var s = await source.OpenSession(cts.Token); - Interlocked.Increment(ref opened); + var s = await source.OpenSession(token); + opened.Inc(); - await Task.Delay(rnd.Next(0, 3), cts.Token); + await Task.Delay(rnd.Next(0, 3), token); s.Close(); - Interlocked.Increment(ref closed); + closed.Inc(); } catch (ObjectDisposedException) { @@ -92,16 +99,15 @@ public async Task Stress_Counts_AreBalanced() } }).ToArray(); - var disposer = Task.Run(async () => await source.DisposeAsync(), cts.Token); + var disposer = Task.Run(async () => await source.DisposeAsync(), token); await Task.WhenAll(workers.Append(disposer)); - Assert.Equal(opened, closed); - Assert.Equal(1, onEmptyCalls); - Assert.True(opened > 0); + Assert.Equal(opened.Value, closed.Value); + Assert.Equal(1, onEmpty.Value); + Assert.True(opened.Value > 0); - await Assert.ThrowsAsync( - () => source.OpenSession(CancellationToken.None).AsTask()); + await Assert.ThrowsAsync(() => source.OpenSession(CancellationToken.None).AsTask()); } [Fact(Timeout = 30_000)] @@ -109,16 +115,17 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() { var driver = DummyDriver(); - var onEmptyCalls = 0; - var source = new ImplicitSessionSource(driver, onEmpty: () => Interlocked.Increment(ref onEmptyCalls)); + var onEmpty = new Counter(); + var source = new ImplicitSessionSource(driver, onEmpty: onEmpty.Inc); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var token = cts.Token; var opens = Enumerable.Range(0, 1000).Select(async _ => { try { - var s = await source.OpenSession(cts.Token); + var s = await source.OpenSession(token); s.Close(); return 1; } @@ -132,13 +139,12 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() { await Task.Yield(); await source.DisposeAsync(); - }, cts.Token); + }, token); await Task.WhenAll(opens.Append(disposeTask)); - Assert.Equal(1, Volatile.Read(ref onEmptyCalls)); + Assert.Equal(1, Volatile.Read(ref onEmpty.Value)); - await Assert.ThrowsAsync( - () => source.OpenSession(CancellationToken.None).AsTask()); + await Assert.ThrowsAsync(() => source.OpenSession(CancellationToken.None).AsTask()); } } From b8db7115f4f285df7785aa190f83682724da4cc4 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 16:11:29 +0300 Subject: [PATCH 22/48] fix --- src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs index 1ff298be..067cc2e9 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs @@ -9,8 +9,8 @@ internal class ImplicitSession : ISession public ImplicitSession(IDriver driver, ImplicitSessionSource source) { - Driver = driver ?? throw new ArgumentNullException(nameof(driver)); - _source = source ?? throw new ArgumentNullException(nameof(source)); + Driver = driver; + _source = source; } public IDriver Driver { get; } From fad0f852bf88f805bb047043bf142700938d4487 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 16:14:36 +0300 Subject: [PATCH 23/48] hot fix --- src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 90fee01e..9de765da 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -9,7 +9,7 @@ internal sealed class ImplicitSessionSource : ISessionSource internal ImplicitSessionSource(IDriver driver, Action? onEmpty = null) { - _driver = driver ?? throw new ArgumentNullException(nameof(driver)); + _driver = driver; _onBecameEmpty = onEmpty; } From 1c2d3baadefbda69042ef07b6f1c8273c1d0aca0 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 16:26:45 +0300 Subject: [PATCH 24/48] hot fix --- src/Ydb.Sdk/src/Ado/PoolManager.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index b6d5a4d3..c4504a10 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -37,11 +37,7 @@ CancellationToken cancellationToken ISessionSource newSessionPool; if (settings.EnableImplicitSession) { - var key = settings.ConnectionString; - newSessionPool = new ImplicitSessionSource( - driver, - onEmpty: () => Pools.TryRemove(key, out _) - ); + newSessionPool = new ImplicitSessionSource(driver); } else { From 04050d60404934cd19982dad06c071448d8bb25a Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 17:12:36 +0300 Subject: [PATCH 25/48] feat: add owner registration and dispose logic for implicit sessions --- src/Ydb.Sdk/src/Ado/PoolManager.cs | 1 + src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs | 5 ++--- .../Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index c4504a10..98fc4d2d 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -37,6 +37,7 @@ CancellationToken cancellationToken ISessionSource newSessionPool; if (settings.EnableImplicitSession) { + driver.RegisterOwner(); newSessionPool = new ImplicitSessionSource(driver); } else diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 9de765da..0a57128d 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -41,7 +41,7 @@ private bool TryAcquireLease() internal void ReleaseLease() => Interlocked.Decrement(ref _activeLeaseCount); - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { Interlocked.Exchange(ref _isDisposed, 1); @@ -51,8 +51,7 @@ public ValueTask DisposeAsync() spinner.SpinOnce(); } + await _driver.DisposeAsync(); _onBecameEmpty?.Invoke(); - - return ValueTask.CompletedTask; } } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs index 23cda97b..076b7291 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs @@ -6,7 +6,12 @@ namespace Ydb.Sdk.Ado.Tests.Session; public class YdbImplicitStressTests : TestBase { - private static IDriver DummyDriver() => new Mock(MockBehavior.Strict).Object; + private static IDriver DummyDriver() + { + var m = new Mock(MockBehavior.Loose); + m.Setup(d => d.DisposeAsync()).Returns(ValueTask.CompletedTask); + return m.Object; + } private sealed class Counter { From 274a0e320268c8b220bb77f2552acf327e7d3f34 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 17:24:25 +0300 Subject: [PATCH 26/48] refactor(ado): remove onEmpty callback from ImplicitSessionSource --- src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 0a57128d..75c1ec26 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -3,14 +3,12 @@ namespace Ydb.Sdk.Ado.Session; internal sealed class ImplicitSessionSource : ISessionSource { private readonly IDriver _driver; - private readonly Action? _onBecameEmpty; private int _isDisposed; private int _activeLeaseCount; - internal ImplicitSessionSource(IDriver driver, Action? onEmpty = null) + internal ImplicitSessionSource(IDriver driver) { _driver = driver; - _onBecameEmpty = onEmpty; } public ValueTask OpenSession(CancellationToken cancellationToken) @@ -52,6 +50,5 @@ public async ValueTask DisposeAsync() } await _driver.DisposeAsync(); - _onBecameEmpty?.Invoke(); } } From 80ac5321b97c58b03586ff31944a4ebde7ed24fd Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 17:24:58 +0300 Subject: [PATCH 27/48] hot fix --- src/Ydb.Sdk/src/Ado/PoolManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index 98fc4d2d..319ea9c2 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -35,14 +35,14 @@ CancellationToken cancellationToken : Drivers[settings.GrpcConnectionString] = await settings.BuildDriver(); ISessionSource newSessionPool; + driver.RegisterOwner(); if (settings.EnableImplicitSession) { - driver.RegisterOwner(); + newSessionPool = new ImplicitSessionSource(driver); } else { - driver.RegisterOwner(); var factory = new PoolingSessionFactory(driver, settings); newSessionPool = new PoolingSessionSource(factory, settings); } From 275d87465c0ecabc380bff4c1211f5f8c6a22c8d Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 17:25:21 +0300 Subject: [PATCH 28/48] refactor --- src/Ydb.Sdk/src/Ado/PoolManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index 319ea9c2..e2cafe67 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -38,7 +38,6 @@ CancellationToken cancellationToken driver.RegisterOwner(); if (settings.EnableImplicitSession) { - newSessionPool = new ImplicitSessionSource(driver); } else From 04a31a6ccef0b2df28e990c6b6a16ff85cc2562f Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 12 Sep 2025 17:49:55 +0300 Subject: [PATCH 29/48] delete onEmpty in stressTest --- .../Session/YdbImplicitStressTests.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs index 076b7291..e8d670c6 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs @@ -28,7 +28,7 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() var opened = new Counter(); var closed = new Counter(); - var source = new ImplicitSessionSource(driver, onEmpty: onEmpty.Inc); + var source = new ImplicitSessionSource(driver); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var token = cts.Token; @@ -64,7 +64,6 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() Assert.True(opened.Value > 0); Assert.Equal(opened.Value, closed.Value); - Assert.Equal(1, Volatile.Read(ref onEmpty.Value)); await Assert.ThrowsAsync(() => source.OpenSession(CancellationToken.None).AsTask()); } @@ -78,7 +77,7 @@ public async Task Stress_Counts_AreBalanced() var closed = new Counter(); var onEmpty = new Counter(); - var source = new ImplicitSessionSource(driver, onEmpty: onEmpty.Inc); + var source = new ImplicitSessionSource(driver); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var token = cts.Token; @@ -109,7 +108,6 @@ public async Task Stress_Counts_AreBalanced() await Task.WhenAll(workers.Append(disposer)); Assert.Equal(opened.Value, closed.Value); - Assert.Equal(1, onEmpty.Value); Assert.True(opened.Value > 0); await Assert.ThrowsAsync(() => source.OpenSession(CancellationToken.None).AsTask()); @@ -121,7 +119,7 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() var driver = DummyDriver(); var onEmpty = new Counter(); - var source = new ImplicitSessionSource(driver, onEmpty: onEmpty.Inc); + var source = new ImplicitSessionSource(driver); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); var token = cts.Token; @@ -148,8 +146,6 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() await Task.WhenAll(opens.Append(disposeTask)); - Assert.Equal(1, Volatile.Read(ref onEmpty.Value)); - await Assert.ThrowsAsync(() => source.OpenSession(CancellationToken.None).AsTask()); } } From f0c9bff7eb6444bd904e3641ca11d85730dcbac0 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Mon, 15 Sep 2025 18:18:30 +0300 Subject: [PATCH 30/48] Refactored implicit session handling and stress tests --- src/EFCore.Ydb/CHANGELOG.md | 1 + src/Ydb.Sdk/src/Ado/PoolManager.cs | 14 +++----- .../src/Ado/Session/ImplicitSessionSource.cs | 24 +++++++++---- .../Ydb.Sdk.Ado.Tests/PoolManagerTests.cs | 5 +-- .../Session/YdbImplicitStressTests.cs | 36 ++++++++----------- 5 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/EFCore.Ydb/CHANGELOG.md b/src/EFCore.Ydb/CHANGELOG.md index df2b9624..ff254360 100644 --- a/src/EFCore.Ydb/CHANGELOG.md +++ b/src/EFCore.Ydb/CHANGELOG.md @@ -1,3 +1,4 @@ +- Added EF provider support for implicit sessions. - Fixed Decimal precision/scale mapping in EF provider. - Supported Guid (Uuid YDB type). - PrivateAssets="none" is set to flow the EF Core analyzer to users referencing this package [issue](https://github.com/aspnet/EntityFrameworkCore/pull/11350). diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index e2cafe67..ae609fa7 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -34,17 +34,11 @@ CancellationToken cancellationToken ? cacheDriver : Drivers[settings.GrpcConnectionString] = await settings.BuildDriver(); - ISessionSource newSessionPool; driver.RegisterOwner(); - if (settings.EnableImplicitSession) - { - newSessionPool = new ImplicitSessionSource(driver); - } - else - { - var factory = new PoolingSessionFactory(driver, settings); - newSessionPool = new PoolingSessionSource(factory, settings); - } + + ISessionSource newSessionPool = settings.EnableImplicitSession + ? new ImplicitSessionSource(driver) + : new PoolingSessionSource(new PoolingSessionFactory(driver, settings), settings); Pools[settings.ConnectionString] = newSessionPool; diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 75c1ec26..f094c518 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -3,6 +3,7 @@ namespace Ydb.Sdk.Ado.Session; internal sealed class ImplicitSessionSource : ISessionSource { private readonly IDriver _driver; + private readonly ManualResetEventSlim _allReleased = new(false); private int _isDisposed; private int _activeLeaseCount; @@ -37,18 +38,27 @@ private bool TryAcquireLease() return true; } - internal void ReleaseLease() => Interlocked.Decrement(ref _activeLeaseCount); + internal void ReleaseLease() + { + if (Interlocked.Decrement(ref _activeLeaseCount) == 0 && Volatile.Read(ref _isDisposed) != 0) + _allReleased.Set(); + } public async ValueTask DisposeAsync() { - Interlocked.Exchange(ref _isDisposed, 1); + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) + return; + + if (Volatile.Read(ref _activeLeaseCount) != 0) + _allReleased.Wait(); - var spinner = new SpinWait(); - while (Volatile.Read(ref _activeLeaseCount) != 0) + try { - spinner.SpinOnce(); + await _driver.DisposeAsync(); + } + finally + { + _allReleased.Dispose(); } - - await _driver.DisposeAsync(); } } diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs index 0b4ba8a9..840ec21a 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/PoolManagerTests.cs @@ -3,10 +3,7 @@ namespace Ydb.Sdk.Ado.Tests; -[CollectionDefinition("PoolManagerTests", DisableParallelization = true)] -public sealed class PoolManagerCollection; - -[Collection("PoolManagerTests")] +[Collection("DisableParallelization")] public class PoolManagerTests { [Theory] diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs index e8d670c6..81c7f6c6 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs @@ -24,26 +24,24 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() { var driver = DummyDriver(); - var onEmpty = new Counter(); - var opened = new Counter(); - var closed = new Counter(); + var opened = new Counter(); + var closed = new Counter(); var source = new ImplicitSessionSource(driver); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var token = cts.Token; - var workers = Enumerable.Range(0, 200).Select(async i => + var workers = Enumerable.Range(0, 200).Select(async _ => { - var rnd = new Random(i ^ Environment.TickCount); + var rnd = Random.Shared; for (var j = 0; j < 10; j++) { try { - var s = await source.OpenSession(token); + var s = await source.OpenSession(cts.Token); opened.Inc(); - await Task.Delay(rnd.Next(0, 5), token); + await Task.Delay(rnd.Next(0, 5), cts.Token); s.Close(); closed.Inc(); @@ -56,9 +54,9 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() var disposer = Task.Run(async () => { - await Task.Delay(10, token); + await Task.Delay(10, cts.Token); await source.DisposeAsync(); - }, token); + }, cts.Token); await Task.WhenAll(workers.Append(disposer)); @@ -75,24 +73,22 @@ public async Task Stress_Counts_AreBalanced() var opened = new Counter(); var closed = new Counter(); - var onEmpty = new Counter(); var source = new ImplicitSessionSource(driver); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var token = cts.Token; - var workers = Enumerable.Range(0, 200).Select(async i => + var workers = Enumerable.Range(0, 200).Select(async _ => { - var rnd = new Random(i ^ Environment.TickCount); + var rnd = Random.Shared; for (var j = 0; j < 10; j++) { try { - var s = await source.OpenSession(token); + var s = await source.OpenSession(cts.Token); opened.Inc(); - await Task.Delay(rnd.Next(0, 3), token); + await Task.Delay(rnd.Next(0, 3), cts.Token); s.Close(); closed.Inc(); @@ -103,7 +99,7 @@ public async Task Stress_Counts_AreBalanced() } }).ToArray(); - var disposer = Task.Run(async () => await source.DisposeAsync(), token); + var disposer = Task.Run(async () => await source.DisposeAsync(), cts.Token); await Task.WhenAll(workers.Append(disposer)); @@ -118,17 +114,15 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() { var driver = DummyDriver(); - var onEmpty = new Counter(); var source = new ImplicitSessionSource(driver); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var token = cts.Token; var opens = Enumerable.Range(0, 1000).Select(async _ => { try { - var s = await source.OpenSession(token); + var s = await source.OpenSession(cts.Token); s.Close(); return 1; } @@ -142,7 +136,7 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() { await Task.Yield(); await source.DisposeAsync(); - }, token); + }, cts.Token); await Task.WhenAll(opens.Append(disposeTask)); From 4075173269a9f8fd9b941dc422634a0898ee8801 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Tue, 12 Aug 2025 19:48:38 +0300 Subject: [PATCH 31/48] feat: add EnableImplicitSession flag support with parsing tests --- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 5 +++ .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 36904da4..daeca47e 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -38,6 +38,11 @@ internal ISession Session } private ISession _session = null!; + + internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; + + internal ISession GetExecutionSession(bool useImplicit) + => useImplicit ? new ImplicitSession(Session.Driver) : Session; public YdbConnection() { diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index 3cf2d07a..05584c38 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -473,4 +473,48 @@ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() await Assert.ThrowsAsync(async () => { await importer.FlushAsync(); }); } + + [Fact] + public void EnableImplicitSession_WhenSetViaPrimaryKey_ParsesAndAppearsInConnectionString() + { + var csb = new YdbConnectionStringBuilder("EnableImplicitSession=true;Host=server;Port=2135;"); + Assert.True(csb.EnableImplicitSession); + + Assert.Contains("EnableImplicitSession=True", csb.ConnectionString); + Assert.Contains("Host=server", csb.ConnectionString); + Assert.Contains("Port=2135", csb.ConnectionString); + } + + [Fact] + public void EnableImplicitSession_WhenSetViaAlias_ParsesAndNormalizesKey() + { + var csb = new YdbConnectionStringBuilder("ImplicitSession=on;Host=server;Port=2135;"); + Assert.True(csb.EnableImplicitSession); + + var s = csb.ConnectionString; + + Assert.Contains("EnableImplicitSession=True", s); + + var parts = s.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + Assert.DoesNotContain(parts, p => p.StartsWith("ImplicitSession=", StringComparison.OrdinalIgnoreCase)); + + Assert.Contains("Host=server", s); + Assert.Contains("Port=2135", s); + } + + [Theory] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("on", true)] + [InlineData("1", true)] + [InlineData("false", false)] + [InlineData("False", false)] + [InlineData("off", false)] + [InlineData("0", false)] + public void EnableImplicitSession_StringBooleanVariants_AreParsed(string value, bool expected) + { + var csb = new YdbConnectionStringBuilder($"EnableImplicitSession={value};"); + Assert.Equal(expected, csb.EnableImplicitSession); + Assert.Contains($"EnableImplicitSession={(expected ? "True" : "False")}", csb.ConnectionString); + } } From bdfdd119e25c7b03772103cc8fcfa299aa92fa6c Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Tue, 12 Aug 2025 23:31:21 +0300 Subject: [PATCH 32/48] fix ci --- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 4 ++-- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index daeca47e..0e1d9e05 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -38,9 +38,9 @@ internal ISession Session } private ISession _session = null!; - + internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; - + internal ISession GetExecutionSession(bool useImplicit) => useImplicit ? new ImplicitSession(Session.Driver) : Session; diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index 05584c38..eb71961e 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -473,7 +473,7 @@ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() await Assert.ThrowsAsync(async () => { await importer.FlushAsync(); }); } - + [Fact] public void EnableImplicitSession_WhenSetViaPrimaryKey_ParsesAndAppearsInConnectionString() { @@ -481,10 +481,10 @@ public void EnableImplicitSession_WhenSetViaPrimaryKey_ParsesAndAppearsInConnect Assert.True(csb.EnableImplicitSession); Assert.Contains("EnableImplicitSession=True", csb.ConnectionString); - Assert.Contains("Host=server", csb.ConnectionString); + Assert.Contains("Host=server", csb.ConnectionString); Assert.Contains("Port=2135", csb.ConnectionString); } - + [Fact] public void EnableImplicitSession_WhenSetViaAlias_ParsesAndNormalizesKey() { @@ -503,14 +503,14 @@ public void EnableImplicitSession_WhenSetViaAlias_ParsesAndNormalizesKey() } [Theory] - [InlineData("true", true)] - [InlineData("True", true)] - [InlineData("on", true)] - [InlineData("1", true)] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("on", true)] + [InlineData("1", true)] [InlineData("false", false)] [InlineData("False", false)] - [InlineData("off", false)] - [InlineData("0", false)] + [InlineData("off", false)] + [InlineData("0", false)] public void EnableImplicitSession_StringBooleanVariants_AreParsed(string value, bool expected) { var csb = new YdbConnectionStringBuilder($"EnableImplicitSession={value};"); From eb6fcb8dde8fbfa69b935ca0db61f727768cafb0 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Thu, 14 Aug 2025 12:45:42 +0300 Subject: [PATCH 33/48] feat: add integration tests and rework implicit session handling via ISessionSource --- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 2 +- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 186 +++++++++++++++--- 2 files changed, 159 insertions(+), 29 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 0e1d9e05..4ed2ce26 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -42,7 +42,7 @@ internal ISession Session internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; internal ISession GetExecutionSession(bool useImplicit) - => useImplicit ? new ImplicitSession(Session.Driver) : Session; + => useImplicit ? PoolManager.GetImplicitSession(ConnectionStringBuilder) : Session; public YdbConnection() { diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index eb71961e..1578eed3 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -475,46 +475,176 @@ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() } [Fact] - public void EnableImplicitSession_WhenSetViaPrimaryKey_ParsesAndAppearsInConnectionString() + public async Task EnableImplicitSession_WhenTrue_AndNoTransaction_UsesImplicitSession() { - var csb = new YdbConnectionStringBuilder("EnableImplicitSession=true;Host=server;Port=2135;"); - Assert.True(csb.EnableImplicitSession); + var cs = ConnectionString + ";EnableImplicitSession=true"; - Assert.Contains("EnableImplicitSession=True", csb.ConnectionString); - Assert.Contains("Host=server", csb.ConnectionString); - Assert.Contains("Port=2135", csb.ConnectionString); + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + + var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1"; + var result = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + Assert.Equal(1L, result); + + var implicitSession = conn.GetExecutionSession(useImplicit: true); + var pooledSession = conn.GetExecutionSession(useImplicit: false); + Assert.NotEqual(implicitSession, pooledSession); } [Fact] - public void EnableImplicitSession_WhenSetViaAlias_ParsesAndNormalizesKey() + public async Task EnableImplicitSession_WhenTrue_ButInsideTransaction_UsesPooledSession() { - var csb = new YdbConnectionStringBuilder("ImplicitSession=on;Host=server;Port=2135;"); - Assert.True(csb.EnableImplicitSession); + var cs = ConnectionString + ";EnableImplicitSession=true"; + + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + + using var tx = conn.BeginTransaction(); + var cmd = conn.CreateCommand(); + cmd.Transaction = tx; + cmd.CommandText = "SELECT 1"; + var result = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + Assert.Equal(1L, result); + + var pooledSession = conn.GetExecutionSession(useImplicit: false); + var implicitSession = conn.GetExecutionSession(useImplicit: true); + + Assert.Equal(pooledSession, conn.Session); + Assert.NotEqual(pooledSession, implicitSession); + } + + [Fact] + public async Task EnableImplicitSession_WhenFalse_AlwaysUsesPooledSession() + { + var cs = ConnectionString + ";EnableImplicitSession=false"; + + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); - var s = csb.ConnectionString; + var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT CAST(1 AS Int64)"; + var result = (long)(await cmd.ExecuteScalarAsync())!; + Assert.Equal(1L, result); - Assert.Contains("EnableImplicitSession=True", s); + var pooledSession = conn.GetExecutionSession(useImplicit: false); + Assert.Equal(pooledSession, conn.Session); + } + + [Fact] + public async Task EnableImplicitSession_DifferentConnectionStrings_HaveDifferentImplicitPools() + { + var cs1 = ConnectionString + ";EnableImplicitSession=true;MinSessionPool=0;DisableDiscovery=false"; + var cs2 = ConnectionString + ";EnableImplicitSession=true;MinSessionPool=1;DisableDiscovery=false"; + + await using var conn1 = new YdbConnection(cs1); + await conn1.OpenAsync(); + var session1 = conn1.GetExecutionSession(useImplicit: true); - var parts = s.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - Assert.DoesNotContain(parts, p => p.StartsWith("ImplicitSession=", StringComparison.OrdinalIgnoreCase)); + await using var conn2 = new YdbConnection(cs2); + await conn2.OpenAsync(); + var session2 = conn2.GetExecutionSession(useImplicit: true); - Assert.Contains("Host=server", s); - Assert.Contains("Port=2135", s); + Assert.NotEqual(session1, session2); } + + [Fact] + public async Task EnableImplicitSession_TwoSequentialCommands_GetDifferentImplicitSessions() + { + var cs = ConnectionString + ";EnableImplicitSession=true"; + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + + var s1 = conn.GetExecutionSession(useImplicit: true); + var s2 = conn.GetExecutionSession(useImplicit: true); - [Theory] - [InlineData("true", true)] - [InlineData("True", true)] - [InlineData("on", true)] - [InlineData("1", true)] - [InlineData("false", false)] - [InlineData("False", false)] - [InlineData("off", false)] - [InlineData("0", false)] - public void EnableImplicitSession_StringBooleanVariants_AreParsed(string value, bool expected) + Assert.NotEqual(s1, s2); + } + + [Fact] + public async Task ClearPool_FireAndForget_DoesNotBlock_And_PoolsRecreate() + { + var csBase = + ConnectionString + + ";UseTls=false" + + ";DisableDiscovery=true" + + ";CreateSessionTimeout=3" + + ";ConnectTimeout=3" + + ";KeepAlivePingDelay=0;KeepAlivePingTimeout=0"; + + var csPooled = csBase; // pooled-пул (без флага) + var csImplicit = csBase + ";EnableImplicitSession=true"; // implicit-пул (с флагом) + + // 1) Прогреваем оба пула (pooled и implicit), чтобы они точно были созданы. + await using (var warmPooled = new YdbConnection(csPooled)) + { + await warmPooled.OpenAsync(); + using var cmd = warmPooled.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); + } + + await using (var warmImplicit = new YdbConnection(csImplicit)) + { + await warmImplicit.OpenAsync(); + using var cmd = warmImplicit.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); + } + + // 2) Вызываем ClearPool для ОБОИХ ключей (по вашей реализации ключи разные). + var clearPooledTask = YdbConnection.ClearPool(new YdbConnection(csPooled)); + var clearImplicitTask = YdbConnection.ClearPool(new YdbConnection(csImplicit)); + + // 3) Убеждаемся, что ClearPool не блокирует — завершается быстро (fail-fast). + // (Если вдруг среда перегружена, можно поднять таймаут до 3–5 секунд.) + var done = await Task.WhenAny(Task.WhenAll(clearPooledTask, clearImplicitTask), Task.Delay(TimeSpan.FromSeconds(2))); + Assert.True(done is not null && done != Task.Delay(TimeSpan.FromSeconds(2)), "ClearPool() must not block."); + + // 4) Проверяем, что пулы корректно пересоздаются после очистки: + // pooled — без флага, implicit — с флагом. + await using (var checkPooled = new YdbConnection(csPooled)) + { + await checkPooled.OpenAsync(); + using var cmd = checkPooled.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); + } + + await using (var checkImplicit = new YdbConnection(csImplicit)) + { + await checkImplicit.OpenAsync(); + using var cmd = checkImplicit.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); + } + } + + [Fact] + public async Task EnableImplicitSession_ParallelQueries_WorkFine() + { + var cs = ConnectionString + ";EnableImplicitSession=true"; + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + + var tasks = Enumerable.Range(0, 16).Select(async _ => + { + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1"; + var v = Convert.ToInt64(await cmd.ExecuteScalarAsync()); + Assert.Equal(1L, v); + }); + await Task.WhenAll(tasks); + } + + [Fact] + public async Task EnableImplicitSession_WithDisableDiscovery_Works() { - var csb = new YdbConnectionStringBuilder($"EnableImplicitSession={value};"); - Assert.Equal(expected, csb.EnableImplicitSession); - Assert.Contains($"EnableImplicitSession={(expected ? "True" : "False")}", csb.ConnectionString); + var cs = ConnectionString + ";EnableImplicitSession=true;DisableDiscovery=true"; + await using var conn = new YdbConnection(cs); + await conn.OpenAsync(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT 1"; + Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); } } From 991a412d093a13ebbdb4c07a7d6730127ed49450 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 3 Sep 2025 03:53:53 +0300 Subject: [PATCH 34/48] feat: update ImplicitSession for singleton driver --- .../test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs index e322da34..bc491a72 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbCommandTests.cs @@ -204,6 +204,122 @@ public async Task ExecuteScalar_WhenSelectNoRows_ReturnNull() .ExecuteScalarAsync()); } + [Fact] + public async Task ImplicitSession_SimpleScalar_Works() + { + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT 40 + 2;"; + var scalar = await cmd.ExecuteScalarAsync(); + Assert.Equal(42, Convert.ToInt32(scalar)); + } + + [Fact] + public async Task ImplicitSession_RepeatedScalars_WorksManyTimes() + { + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + for (var i = 0; i < 30; i++) + { + var cmd = connection.CreateCommand(); + cmd.CommandText = $"SELECT {i};"; + var scalar = await cmd.ExecuteScalarAsync(); + Assert.Equal(i, Convert.ToInt32(scalar)); + } + } + + [Fact] + public void ImplicitSession_ConcurrentCommand_IsStillBlockedByBusyCheck() + { + using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + connection.Open(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT 1; SELECT 1;"; + using var reader = cmd.ExecuteReader(); + + var ex = Assert.Throws(() => cmd.ExecuteReader()); + Assert.Equal("A command is already in progress: SELECT 1; SELECT 1;", ex.Message); + } + + [Fact] + public async Task ImplicitSession_WithExplicitTransaction_UsesExplicitSessionAndCommits() + { + var table = $"Implicit_{Guid.NewGuid():N}"; + + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + try + { + await using (var create = connection.CreateCommand()) + { + create.CommandText = $""" + CREATE TABLE {table} ( + Id Int32, + Name Text, + PRIMARY KEY (Id) + ) + """; + await create.ExecuteNonQueryAsync(); + } + + var tx = connection.BeginTransaction(); + await using (var insert = connection.CreateCommand()) + { + insert.Transaction = tx; + insert.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (1, 'A');"; + await insert.ExecuteNonQueryAsync(); + insert.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (2, 'B');"; + await insert.ExecuteNonQueryAsync(); + } + + await tx.CommitAsync(); + + await using (var check = connection.CreateCommand()) + { + check.CommandText = $"SELECT COUNT(*) FROM {table};"; + var count = Convert.ToInt32(await check.ExecuteScalarAsync()); + Assert.Equal(2, count); + } + } + finally + { + await using var drop = connection.CreateCommand(); + drop.CommandText = $"DROP TABLE {table}"; + await drop.ExecuteNonQueryAsync(); + } + } + + [Fact] + public async Task ImplicitSession_Cancellation_AfterFirstResult_StillReturnsFirst() + { + await using var connection = CreateConnection(); + connection.ConnectionString += ";EnableImplicitSession=true"; + await connection.OpenAsync(); + + var cmd = new YdbCommand(connection) { CommandText = "SELECT 1; SELECT 1;" }; + using var cts = new CancellationTokenSource(); + + var reader = await cmd.ExecuteReaderAsync(cts.Token); + + await reader.ReadAsync(cts.Token); + Assert.Equal(1, reader.GetValue(0)); + Assert.True(await reader.NextResultAsync(cts.Token)); + + await cts.CancelAsync(); + + await reader.ReadAsync(cts.Token); + Assert.Equal(1, reader.GetValue(0)); + Assert.False(await reader.NextResultAsync()); + } public class Data(DbType dbType, T expected, bool isNullable = false) { From 04f41b013c987f6593f1e63df8286d50976c8ddf Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Tue, 16 Sep 2025 13:17:34 +0300 Subject: [PATCH 35/48] test --- src/EFCore.Ydb/CHANGELOG.md | 1 - src/Ydb.Sdk/CHANGELOG.md | 1 + src/Ydb.Sdk/src/Ado/PoolManager.cs | 8 +-- .../src/Ado/Session/ImplicitSession.cs | 2 +- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 33 ++++++++-- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 30 ++-------- src/Ydb.Sdk/src/Ado/YdbDataSource.cs | 9 +-- .../Session/YdbImplicitStressTests.cs | 6 +- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 60 +++++++++---------- 9 files changed, 76 insertions(+), 74 deletions(-) diff --git a/src/EFCore.Ydb/CHANGELOG.md b/src/EFCore.Ydb/CHANGELOG.md index ff254360..df2b9624 100644 --- a/src/EFCore.Ydb/CHANGELOG.md +++ b/src/EFCore.Ydb/CHANGELOG.md @@ -1,4 +1,3 @@ -- Added EF provider support for implicit sessions. - Fixed Decimal precision/scale mapping in EF provider. - Supported Guid (Uuid YDB type). - PrivateAssets="none" is set to flow the EF Core analyzer to users referencing this package [issue](https://github.com/aspnet/EntityFrameworkCore/pull/11350). diff --git a/src/Ydb.Sdk/CHANGELOG.md b/src/Ydb.Sdk/CHANGELOG.md index ec28d670..488507d1 100644 --- a/src/Ydb.Sdk/CHANGELOG.md +++ b/src/Ydb.Sdk/CHANGELOG.md @@ -1,3 +1,4 @@ +- Added provider support for implicit sessions. - Feat ADO.NET: `YdbDataSource.OpenRetryableConnectionAsync` opens a retryable connection with automatic retries for transient failures. - Fixed bug ADO.NET/PoolManager: `SemaphoreSlim.WaitAsync` over-release on cancellation. - Feat ADO.NET: Mark `YdbConnection.State` as `Broken` when the underlying session is broken, including background deactivation. diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index d27fbc40..ae609fa7 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -10,14 +10,14 @@ internal static class PoolManager internal static readonly ConcurrentDictionary Drivers = new(); internal static readonly ConcurrentDictionary Pools = new(); - internal static async ValueTask Get( + internal static async Task GetSession( YdbConnectionStringBuilder settings, CancellationToken cancellationToken ) { if (Pools.TryGetValue(settings.ConnectionString, out var sessionPool)) { - return sessionPool; + return await sessionPool.OpenSession(cancellationToken); } await SemaphoreSlim.WaitAsync(cancellationToken); @@ -26,7 +26,7 @@ CancellationToken cancellationToken { if (Pools.TryGetValue(settings.ConnectionString, out var pool)) { - return pool; + return await pool.OpenSession(cancellationToken); } var driver = Drivers.TryGetValue(settings.GrpcConnectionString, out var cacheDriver) && @@ -42,7 +42,7 @@ CancellationToken cancellationToken Pools[settings.ConnectionString] = newSessionPool; - return newSessionPool; + return await newSessionPool.OpenSession(cancellationToken); } finally { diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs index e2bd0bcb..34fab45d 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSession.cs @@ -50,7 +50,7 @@ public void OnNotSuccessStatusCode(StatusCode code) { } - public void Close() => _source.ReleaseLease(); + public void Dispose() => _source.ReleaseLease(); private static YdbException NotSupportedTransaction => new("Transactions are not supported in implicit sessions"); } diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index c8618a54..2fa0c18b 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using Ydb.Sdk.Ado.Internal; +using Ydb.Sdk.Ado.Session; namespace Ydb.Sdk.Ado; @@ -211,14 +212,38 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha var transaction = YdbConnection.CurrentTransaction; - if (Transaction != null && Transaction != transaction) // assert on legacy DbTransaction property + if (Transaction != null && Transaction != transaction) { throw new InvalidOperationException("Transaction mismatched! (Maybe using another connection)"); } - var ydbDataReader = await YdbDataReader.CreateYdbDataReader(await YdbConnection.Session.ExecuteQuery( - preparedSql.ToString(), ydbParameters, execSettings, transaction?.TransactionControl - ), YdbConnection.OnNotSuccessStatusCode, transaction, cancellationToken); + var useImplicit = YdbConnection.EnableImplicitSession && transaction is null; + var session = useImplicit + ? new ImplicitSession(YdbConnection.Session.Driver, new ImplicitSessionSource(YdbConnection.Session.Driver)) + : YdbConnection.Session; + + YdbDataReader ydbDataReader; + try + { + var execResult = await session.ExecuteQuery( + preparedSql.ToString(), + ydbParameters, + execSettings, + transaction?.TransactionControl + ); + + ydbDataReader = await YdbDataReader.CreateYdbDataReader( + execResult, + YdbConnection.OnNotSuccessStatusCode, + transaction, + cancellationToken + ); + } + finally + { + if (useImplicit) + session.Dispose(); + } YdbConnection.LastReader = ydbDataReader; YdbConnection.LastCommand = CommandText; diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 4ed2ce26..3136fb6d 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -2,7 +2,6 @@ using System.Data.Common; using System.Diagnostics.CodeAnalysis; using Ydb.Sdk.Ado.BulkUpsert; -using Ydb.Sdk.Ado.RetryPolicy; using Ydb.Sdk.Ado.Session; using static System.Data.IsolationLevel; @@ -39,11 +38,6 @@ internal ISession Session private ISession _session = null!; - internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; - - internal ISession GetExecutionSession(bool useImplicit) - => useImplicit ? PoolManager.GetImplicitSession(ConnectionStringBuilder) : Session; - public YdbConnection() { } @@ -87,7 +81,11 @@ public YdbTransaction BeginTransaction(TransactionMode transactionMode = Transac return CurrentTransaction; } - public override void ChangeDatabase(string databaseName) => throw new NotSupportedException(); + public override void ChangeDatabase(string databaseName) + { + } + + internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; public override void Close() => CloseAsync().GetAwaiter().GetResult(); @@ -97,29 +95,13 @@ public override async Task OpenAsync(CancellationToken cancellationToken) { ThrowIfConnectionOpen(); - var sessionSource = await PoolManager.Get(ConnectionStringBuilder, cancellationToken); - - Session = await sessionSource.OpenSession(cancellationToken); + Session = await PoolManager.GetSession(ConnectionStringBuilder, cancellationToken); OnStateChange(ClosedToOpenEventArgs); ConnectionState = ConnectionState.Open; } - internal async ValueTask OpenAsync( - YdbRetryPolicyExecutor retryPolicyExecutor, - CancellationToken cancellationToken = default - ) - { - ThrowIfConnectionOpen(); - - var sessionSource = await PoolManager.Get(ConnectionStringBuilder, cancellationToken); - - Session = new RetryableSession(sessionSource, retryPolicyExecutor); - - ConnectionState = ConnectionState.Open; - } - public override async Task CloseAsync() { // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault diff --git a/src/Ydb.Sdk/src/Ado/YdbDataSource.cs b/src/Ydb.Sdk/src/Ado/YdbDataSource.cs index d37e94c3..2734b613 100644 --- a/src/Ydb.Sdk/src/Ado/YdbDataSource.cs +++ b/src/Ydb.Sdk/src/Ado/YdbDataSource.cs @@ -390,8 +390,7 @@ public async ValueTask OpenRetryableConnectionAsync(CancellationT var ydbConnection = CreateDbConnection(); try { - await ydbConnection.OpenAsync(_retryPolicyExecutor, cancellationToken); - + await ydbConnection.OpenAsync(cancellationToken); return ydbConnection; } catch @@ -409,8 +408,7 @@ public async ValueTask OpenRetryableConnectionAsync( var ydbConnection = CreateDbConnection(); try { - await ydbConnection.OpenAsync(GetExecutor(ydbRetryPolicyConfig), cancellationToken); - + await ydbConnection.OpenAsync(cancellationToken); return ydbConnection; } catch @@ -428,8 +426,7 @@ public async ValueTask OpenRetryableConnectionAsync( var ydbConnection = CreateDbConnection(); try { - await ydbConnection.OpenAsync(new YdbRetryPolicyExecutor(retryPolicy), cancellationToken); - + await ydbConnection.OpenAsync(cancellationToken); return ydbConnection; } catch diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs index 81c7f6c6..4a4ed6c4 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs @@ -43,7 +43,7 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() await Task.Delay(rnd.Next(0, 5), cts.Token); - s.Close(); + s.Dispose(); closed.Inc(); } catch (ObjectDisposedException) @@ -90,7 +90,7 @@ public async Task Stress_Counts_AreBalanced() await Task.Delay(rnd.Next(0, 3), cts.Token); - s.Close(); + s.Dispose(); closed.Inc(); } catch (ObjectDisposedException) @@ -123,7 +123,7 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() try { var s = await source.OpenSession(cts.Token); - s.Close(); + s.Dispose(); return 1; } catch (ObjectDisposedException) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index 1578eed3..1651902b 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -1,5 +1,6 @@ using System.Data; using Xunit; +using Ydb.Sdk.Ado.Session; using Ydb.Sdk.Ado.Tests.Utils; using Ydb.Sdk.Ado.YdbType; @@ -487,9 +488,7 @@ public async Task EnableImplicitSession_WhenTrue_AndNoTransaction_UsesImplicitSe var result = Convert.ToInt64(await cmd.ExecuteScalarAsync()); Assert.Equal(1L, result); - var implicitSession = conn.GetExecutionSession(useImplicit: true); - var pooledSession = conn.GetExecutionSession(useImplicit: false); - Assert.NotEqual(implicitSession, pooledSession); + Assert.IsType(conn.Session); } [Fact] @@ -507,11 +506,7 @@ public async Task EnableImplicitSession_WhenTrue_ButInsideTransaction_UsesPooled var result = Convert.ToInt64(await cmd.ExecuteScalarAsync()); Assert.Equal(1L, result); - var pooledSession = conn.GetExecutionSession(useImplicit: false); - var implicitSession = conn.GetExecutionSession(useImplicit: true); - - Assert.Equal(pooledSession, conn.Session); - Assert.NotEqual(pooledSession, implicitSession); + Assert.IsNotType(conn.Session); } [Fact] @@ -527,8 +522,7 @@ public async Task EnableImplicitSession_WhenFalse_AlwaysUsesPooledSession() var result = (long)(await cmd.ExecuteScalarAsync())!; Assert.Equal(1L, result); - var pooledSession = conn.GetExecutionSession(useImplicit: false); - Assert.Equal(pooledSession, conn.Session); + Assert.IsNotType(conn.Session); } [Fact] @@ -539,28 +533,38 @@ public async Task EnableImplicitSession_DifferentConnectionStrings_HaveDifferent await using var conn1 = new YdbConnection(cs1); await conn1.OpenAsync(); - var session1 = conn1.GetExecutionSession(useImplicit: true); + var s1 = conn1.Session; await using var conn2 = new YdbConnection(cs2); await conn2.OpenAsync(); - var session2 = conn2.GetExecutionSession(useImplicit: true); + var s2 = conn2.Session; - Assert.NotEqual(session1, session2); + Assert.NotEqual(s1, s2); } - + [Fact] - public async Task EnableImplicitSession_TwoSequentialCommands_GetDifferentImplicitSessions() + public async Task EnableImplicitSession_TwoSequentialCommands_ReusesSameSession() { var cs = ConnectionString + ";EnableImplicitSession=true"; await using var conn = new YdbConnection(cs); await conn.OpenAsync(); - var s1 = conn.GetExecutionSession(useImplicit: true); - var s2 = conn.GetExecutionSession(useImplicit: true); + var cmd1 = conn.CreateCommand(); + cmd1.CommandText = "SELECT 1;"; + var result1 = await cmd1.ExecuteScalarAsync(); - Assert.NotEqual(s1, s2); + var s1 = conn.Session; + + var cmd2 = conn.CreateCommand(); + cmd2.CommandText = "SELECT 2;"; + var result2 = await cmd2.ExecuteScalarAsync(); + + var s2 = conn.Session; + + Assert.Equal(s1, s2); + } - + [Fact] public async Task ClearPool_FireAndForget_DoesNotBlock_And_PoolsRecreate() { @@ -572,10 +576,9 @@ public async Task ClearPool_FireAndForget_DoesNotBlock_And_PoolsRecreate() ";ConnectTimeout=3" + ";KeepAlivePingDelay=0;KeepAlivePingTimeout=0"; - var csPooled = csBase; // pooled-пул (без флага) - var csImplicit = csBase + ";EnableImplicitSession=true"; // implicit-пул (с флагом) + var csPooled = csBase; + var csImplicit = csBase + ";EnableImplicitSession=true"; - // 1) Прогреваем оба пула (pooled и implicit), чтобы они точно были созданы. await using (var warmPooled = new YdbConnection(csPooled)) { await warmPooled.OpenAsync(); @@ -592,21 +595,16 @@ public async Task ClearPool_FireAndForget_DoesNotBlock_And_PoolsRecreate() Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); } - // 2) Вызываем ClearPool для ОБОИХ ключей (по вашей реализации ключи разные). var clearPooledTask = YdbConnection.ClearPool(new YdbConnection(csPooled)); var clearImplicitTask = YdbConnection.ClearPool(new YdbConnection(csImplicit)); - // 3) Убеждаемся, что ClearPool не блокирует — завершается быстро (fail-fast). - // (Если вдруг среда перегружена, можно поднять таймаут до 3–5 секунд.) var done = await Task.WhenAny(Task.WhenAll(clearPooledTask, clearImplicitTask), Task.Delay(TimeSpan.FromSeconds(2))); Assert.True(done is not null && done != Task.Delay(TimeSpan.FromSeconds(2)), "ClearPool() must not block."); - // 4) Проверяем, что пулы корректно пересоздаются после очистки: - // pooled — без флага, implicit — с флагом. await using (var checkPooled = new YdbConnection(csPooled)) { await checkPooled.OpenAsync(); - using var cmd = checkPooled.CreateCommand(); + await using var cmd = checkPooled.CreateCommand(); cmd.CommandText = "SELECT 1"; Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); } @@ -614,12 +612,12 @@ public async Task ClearPool_FireAndForget_DoesNotBlock_And_PoolsRecreate() await using (var checkImplicit = new YdbConnection(csImplicit)) { await checkImplicit.OpenAsync(); - using var cmd = checkImplicit.CreateCommand(); + await using var cmd = checkImplicit.CreateCommand(); cmd.CommandText = "SELECT 1"; Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); } } - + [Fact] public async Task EnableImplicitSession_ParallelQueries_WorkFine() { @@ -636,7 +634,7 @@ public async Task EnableImplicitSession_ParallelQueries_WorkFine() }); await Task.WhenAll(tasks); } - + [Fact] public async Task EnableImplicitSession_WithDisableDiscovery_Works() { From ed0de428bf1e2d68e3331a43fbafb308e68c2aed Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 17 Sep 2025 10:00:17 +0300 Subject: [PATCH 36/48] test --- .../Ydb.Sdk.Ado.Tests/{Session => }/YdbImplictConnectionTests.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/{Session => }/YdbImplictConnectionTests.cs (100%) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplictConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs similarity index 100% rename from src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplictConnectionTests.cs rename to src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs From ef47d688d6194e23c5ba3c5ba082c56576b48703 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 17 Sep 2025 13:48:55 +0300 Subject: [PATCH 37/48] test --- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 2 +- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 2fa0c18b..48548c8a 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -210,7 +210,7 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha ? new GrpcRequestSettings { TransportTimeout = TimeSpan.FromSeconds(CommandTimeout) } : new GrpcRequestSettings(); - var transaction = YdbConnection.CurrentTransaction; + var transaction = Transaction ?? YdbConnection.CurrentTransaction; if (Transaction != null && Transaction != transaction) { diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 3136fb6d..ab6fbf57 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -72,8 +72,17 @@ public YdbTransaction BeginTransaction(TransactionMode transactionMode = Transac if (CurrentTransaction is { Completed: false }) { throw new InvalidOperationException( - "A transaction is already in progress; nested/concurrent transactions aren't supported." - ); + "A transaction is already in progress; nested/concurrent transactions aren't supported."); + } + + if (Session is ImplicitSession) + { + var driver = Session.Driver; + Session.Dispose(); + + var factory = new PoolingSessionFactory(driver, ConnectionStringBuilder); + var pooledSource = new PoolingSessionSource(factory, ConnectionStringBuilder); + Session = pooledSource.OpenSession(CancellationToken.None).GetAwaiter().GetResult(); } CurrentTransaction = new YdbTransaction(this, transactionMode); From 0d16841e2d40b2c4759ef0083537f3db2499e0a0 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 17 Sep 2025 14:50:44 +0300 Subject: [PATCH 38/48] Revert "test" This reverts commit ef47d688d6194e23c5ba3c5ba082c56576b48703. --- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 2 +- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 48548c8a..2fa0c18b 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -210,7 +210,7 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha ? new GrpcRequestSettings { TransportTimeout = TimeSpan.FromSeconds(CommandTimeout) } : new GrpcRequestSettings(); - var transaction = Transaction ?? YdbConnection.CurrentTransaction; + var transaction = YdbConnection.CurrentTransaction; if (Transaction != null && Transaction != transaction) { diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index ab6fbf57..3136fb6d 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -72,17 +72,8 @@ public YdbTransaction BeginTransaction(TransactionMode transactionMode = Transac if (CurrentTransaction is { Completed: false }) { throw new InvalidOperationException( - "A transaction is already in progress; nested/concurrent transactions aren't supported."); - } - - if (Session is ImplicitSession) - { - var driver = Session.Driver; - Session.Dispose(); - - var factory = new PoolingSessionFactory(driver, ConnectionStringBuilder); - var pooledSource = new PoolingSessionSource(factory, ConnectionStringBuilder); - Session = pooledSource.OpenSession(CancellationToken.None).GetAwaiter().GetResult(); + "A transaction is already in progress; nested/concurrent transactions aren't supported." + ); } CurrentTransaction = new YdbTransaction(this, transactionMode); From d058f9292cde50277eac1405ade897f43937b530 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 17 Sep 2025 15:43:41 +0300 Subject: [PATCH 39/48] test --- src/Ydb.Sdk/src/Ado/PoolManager.cs | 13 ++--- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 32 ++--------- src/Ydb.Sdk/src/Ado/YdbConnection.cs | 27 ++++++--- src/Ydb.Sdk/src/Ado/YdbDataSource.cs | 8 +-- .../YdbImplictConnectionTests.cs | 55 ------------------- 5 files changed, 34 insertions(+), 101 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index ae609fa7..457df53e 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -10,14 +10,14 @@ internal static class PoolManager internal static readonly ConcurrentDictionary Drivers = new(); internal static readonly ConcurrentDictionary Pools = new(); - internal static async Task GetSession( + internal static async ValueTask Get( YdbConnectionStringBuilder settings, CancellationToken cancellationToken ) { if (Pools.TryGetValue(settings.ConnectionString, out var sessionPool)) { - return await sessionPool.OpenSession(cancellationToken); + return sessionPool; } await SemaphoreSlim.WaitAsync(cancellationToken); @@ -26,7 +26,7 @@ CancellationToken cancellationToken { if (Pools.TryGetValue(settings.ConnectionString, out var pool)) { - return await pool.OpenSession(cancellationToken); + return pool; } var driver = Drivers.TryGetValue(settings.GrpcConnectionString, out var cacheDriver) && @@ -36,13 +36,12 @@ CancellationToken cancellationToken driver.RegisterOwner(); - ISessionSource newSessionPool = settings.EnableImplicitSession - ? new ImplicitSessionSource(driver) - : new PoolingSessionSource(new PoolingSessionFactory(driver, settings), settings); + var factory = new PoolingSessionFactory(driver, settings); + var newSessionPool = new PoolingSessionSource(factory, settings); Pools[settings.ConnectionString] = newSessionPool; - return await newSessionPool.OpenSession(cancellationToken); + return newSessionPool; } finally { diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 2fa0c18b..84034ad2 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -212,38 +212,14 @@ protected override async Task ExecuteDbDataReaderAsync(CommandBeha var transaction = YdbConnection.CurrentTransaction; - if (Transaction != null && Transaction != transaction) + if (Transaction != null && Transaction != transaction) // assert on legacy DbTransaction property { throw new InvalidOperationException("Transaction mismatched! (Maybe using another connection)"); } - var useImplicit = YdbConnection.EnableImplicitSession && transaction is null; - var session = useImplicit - ? new ImplicitSession(YdbConnection.Session.Driver, new ImplicitSessionSource(YdbConnection.Session.Driver)) - : YdbConnection.Session; - - YdbDataReader ydbDataReader; - try - { - var execResult = await session.ExecuteQuery( - preparedSql.ToString(), - ydbParameters, - execSettings, - transaction?.TransactionControl - ); - - ydbDataReader = await YdbDataReader.CreateYdbDataReader( - execResult, - YdbConnection.OnNotSuccessStatusCode, - transaction, - cancellationToken - ); - } - finally - { - if (useImplicit) - session.Dispose(); - } + var ydbDataReader = await YdbDataReader.CreateYdbDataReader(await YdbConnection.Session.ExecuteQuery( + preparedSql.ToString(), ydbParameters, execSettings, transaction?.TransactionControl + ), YdbConnection.OnNotSuccessStatusCode, transaction, cancellationToken); YdbConnection.LastReader = ydbDataReader; YdbConnection.LastCommand = CommandText; diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 3136fb6d..25c0ae2f 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -2,6 +2,7 @@ using System.Data.Common; using System.Diagnostics.CodeAnalysis; using Ydb.Sdk.Ado.BulkUpsert; +using Ydb.Sdk.Ado.RetryPolicy; using Ydb.Sdk.Ado.Session; using static System.Data.IsolationLevel; @@ -81,11 +82,7 @@ public YdbTransaction BeginTransaction(TransactionMode transactionMode = Transac return CurrentTransaction; } - public override void ChangeDatabase(string databaseName) - { - } - - internal bool EnableImplicitSession => ConnectionStringBuilder.EnableImplicitSession; + public override void ChangeDatabase(string databaseName) => throw new NotSupportedException(); public override void Close() => CloseAsync().GetAwaiter().GetResult(); @@ -95,12 +92,28 @@ public override async Task OpenAsync(CancellationToken cancellationToken) { ThrowIfConnectionOpen(); - Session = await PoolManager.GetSession(ConnectionStringBuilder, cancellationToken); + var sessionSource = await PoolManager.Get(ConnectionStringBuilder, cancellationToken); + + Session = await sessionSource.OpenSession(cancellationToken); OnStateChange(ClosedToOpenEventArgs); ConnectionState = ConnectionState.Open; } + + internal async ValueTask OpenAsync( + YdbRetryPolicyExecutor retryPolicyExecutor, + CancellationToken cancellationToken = default + ) + { + ThrowIfConnectionOpen(); + + var sessionSource = await PoolManager.Get(ConnectionStringBuilder, cancellationToken); + + Session = new RetryableSession(sessionSource, retryPolicyExecutor); + + ConnectionState = ConnectionState.Open; + } public override async Task CloseAsync() { @@ -160,7 +173,7 @@ public override string ConnectionString ? ConnectionState.Broken : ConnectionState; - private ConnectionState ConnectionState { get; set; } = ConnectionState.Closed; + private ConnectionState ConnectionState { get; set; } = ConnectionState.Closed; // Invoke AsyncOpen() internal void OnNotSuccessStatusCode(StatusCode code) => _session.OnNotSuccessStatusCode(code); diff --git a/src/Ydb.Sdk/src/Ado/YdbDataSource.cs b/src/Ydb.Sdk/src/Ado/YdbDataSource.cs index 2734b613..1f395d9f 100644 --- a/src/Ydb.Sdk/src/Ado/YdbDataSource.cs +++ b/src/Ydb.Sdk/src/Ado/YdbDataSource.cs @@ -88,7 +88,7 @@ async ValueTask OpenConnectionAsync(CancellationToken cancellatio try { - await ydbConnection.OpenAsync(cancellationToken); + await ydbConnection.OpenAsync(_retryPolicyExecutor, cancellationToken); return ydbConnection; } catch @@ -390,7 +390,7 @@ public async ValueTask OpenRetryableConnectionAsync(CancellationT var ydbConnection = CreateDbConnection(); try { - await ydbConnection.OpenAsync(cancellationToken); + await ydbConnection.OpenAsync(_retryPolicyExecutor, cancellationToken); return ydbConnection; } catch @@ -408,7 +408,7 @@ public async ValueTask OpenRetryableConnectionAsync( var ydbConnection = CreateDbConnection(); try { - await ydbConnection.OpenAsync(cancellationToken); + await ydbConnection.OpenAsync(_retryPolicyExecutor, cancellationToken); return ydbConnection; } catch @@ -426,7 +426,7 @@ public async ValueTask OpenRetryableConnectionAsync( var ydbConnection = CreateDbConnection(); try { - await ydbConnection.OpenAsync(cancellationToken); + await ydbConnection.OpenAsync(_retryPolicyExecutor, cancellationToken); return ydbConnection; } catch diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs index 6f3124f6..0f3f2c8c 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs @@ -48,61 +48,6 @@ public void ImplicitSession_ConcurrentCommand_IsStillBlockedByBusyCheck() Assert.Equal("A command is already in progress: SELECT 1; SELECT 1;", ex.Message); } - [Fact] - public async Task ImplicitSession_DisallowsTransactions_And_AllowsNonTransactionalCommands() - { - var table = $"Implicit_{Guid.NewGuid():N}"; - - await using var connection = CreateConnection(); - connection.ConnectionString += ";EnableImplicitSession=true"; - await connection.OpenAsync(); - - try - { - await using (var create = connection.CreateCommand()) - { - create.CommandText = $""" - CREATE TABLE {table} ( - Id Int32, - Name Text, - PRIMARY KEY (Id) - ) - """; - await create.ExecuteNonQueryAsync(); - } - - await using (var insert = connection.CreateCommand()) - { - insert.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (1, 'A');"; - await insert.ExecuteNonQueryAsync(); - } - - var tx = connection.BeginTransaction(); - await using (var insertTx = connection.CreateCommand()) - { - insertTx.Transaction = tx; - insertTx.CommandText = $"INSERT INTO {table} (Id, Name) VALUES (2, 'B');"; - var ex = await Assert.ThrowsAsync(async () => await insertTx.ExecuteNonQueryAsync()); - Assert.Contains("Transactions are not supported in implicit sessions", ex.Message); - } - - await tx.RollbackAsync(); - - await using (var check = connection.CreateCommand()) - { - check.CommandText = $"SELECT COUNT(*) FROM {table};"; - var count = Convert.ToInt32(await check.ExecuteScalarAsync()); - Assert.Equal(1, count); - } - } - finally - { - await using var drop = connection.CreateCommand(); - drop.CommandText = $"DROP TABLE {table}"; - await drop.ExecuteNonQueryAsync(); - } - } - [Fact] public async Task ImplicitSession_Cancellation_AfterFirstResult_StillReturnsFirst() { From c4158c22d2733fedbf6ff4f51496b65ea57256d4 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 17 Sep 2025 16:10:49 +0300 Subject: [PATCH 40/48] test --- .../Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index 1651902b..6dbc38cb 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -475,22 +475,6 @@ public async Task BulkUpsertImporter_ThrowsOnNonexistentTable() await Assert.ThrowsAsync(async () => { await importer.FlushAsync(); }); } - [Fact] - public async Task EnableImplicitSession_WhenTrue_AndNoTransaction_UsesImplicitSession() - { - var cs = ConnectionString + ";EnableImplicitSession=true"; - - await using var conn = new YdbConnection(cs); - await conn.OpenAsync(); - - var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT 1"; - var result = Convert.ToInt64(await cmd.ExecuteScalarAsync()); - Assert.Equal(1L, result); - - Assert.IsType(conn.Session); - } - [Fact] public async Task EnableImplicitSession_WhenTrue_ButInsideTransaction_UsesPooledSession() { @@ -618,23 +602,6 @@ public async Task ClearPool_FireAndForget_DoesNotBlock_And_PoolsRecreate() } } - [Fact] - public async Task EnableImplicitSession_ParallelQueries_WorkFine() - { - var cs = ConnectionString + ";EnableImplicitSession=true"; - await using var conn = new YdbConnection(cs); - await conn.OpenAsync(); - - var tasks = Enumerable.Range(0, 16).Select(async _ => - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = "SELECT 1"; - var v = Convert.ToInt64(await cmd.ExecuteScalarAsync()); - Assert.Equal(1L, v); - }); - await Task.WhenAll(tasks); - } - [Fact] public async Task EnableImplicitSession_WithDisableDiscovery_Works() { From 622b48ed2e3b037946f25bebbeff0d7463530f3f Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 17 Sep 2025 16:53:01 +0300 Subject: [PATCH 41/48] test --- src/Ydb.Sdk/src/Ado/YdbCommand.cs | 1 - src/Ydb.Sdk/src/Ado/YdbConnection.cs | 2 +- src/Ydb.Sdk/src/Ado/YdbDataSource.cs | 2 +- .../test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs | 12 ++++++------ .../Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/YdbCommand.cs b/src/Ydb.Sdk/src/Ado/YdbCommand.cs index 84034ad2..c8618a54 100644 --- a/src/Ydb.Sdk/src/Ado/YdbCommand.cs +++ b/src/Ydb.Sdk/src/Ado/YdbCommand.cs @@ -3,7 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using Ydb.Sdk.Ado.Internal; -using Ydb.Sdk.Ado.Session; namespace Ydb.Sdk.Ado; diff --git a/src/Ydb.Sdk/src/Ado/YdbConnection.cs b/src/Ydb.Sdk/src/Ado/YdbConnection.cs index 25c0ae2f..efd44cad 100644 --- a/src/Ydb.Sdk/src/Ado/YdbConnection.cs +++ b/src/Ydb.Sdk/src/Ado/YdbConnection.cs @@ -100,7 +100,7 @@ public override async Task OpenAsync(CancellationToken cancellationToken) ConnectionState = ConnectionState.Open; } - + internal async ValueTask OpenAsync( YdbRetryPolicyExecutor retryPolicyExecutor, CancellationToken cancellationToken = default diff --git a/src/Ydb.Sdk/src/Ado/YdbDataSource.cs b/src/Ydb.Sdk/src/Ado/YdbDataSource.cs index 1f395d9f..a4fdf0c9 100644 --- a/src/Ydb.Sdk/src/Ado/YdbDataSource.cs +++ b/src/Ydb.Sdk/src/Ado/YdbDataSource.cs @@ -88,7 +88,7 @@ async ValueTask OpenConnectionAsync(CancellationToken cancellatio try { - await ydbConnection.OpenAsync(_retryPolicyExecutor, cancellationToken); + await ydbConnection.OpenAsync(cancellationToken); return ydbConnection; } catch diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs index 6dbc38cb..0e985d35 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbConnectionTests.cs @@ -535,18 +535,17 @@ public async Task EnableImplicitSession_TwoSequentialCommands_ReusesSameSession( var cmd1 = conn.CreateCommand(); cmd1.CommandText = "SELECT 1;"; - var result1 = await cmd1.ExecuteScalarAsync(); + await cmd1.ExecuteScalarAsync(); var s1 = conn.Session; var cmd2 = conn.CreateCommand(); cmd2.CommandText = "SELECT 2;"; - var result2 = await cmd2.ExecuteScalarAsync(); + await cmd2.ExecuteScalarAsync(); var s2 = conn.Session; Assert.Equal(s1, s2); - } [Fact] @@ -579,11 +578,12 @@ public async Task ClearPool_FireAndForget_DoesNotBlock_And_PoolsRecreate() Assert.Equal(1L, Convert.ToInt64(await cmd.ExecuteScalarAsync())); } - var clearPooledTask = YdbConnection.ClearPool(new YdbConnection(csPooled)); + var clearPooledTask = YdbConnection.ClearPool(new YdbConnection(csPooled)); var clearImplicitTask = YdbConnection.ClearPool(new YdbConnection(csImplicit)); - var done = await Task.WhenAny(Task.WhenAll(clearPooledTask, clearImplicitTask), Task.Delay(TimeSpan.FromSeconds(2))); - Assert.True(done is not null && done != Task.Delay(TimeSpan.FromSeconds(2)), "ClearPool() must not block."); + var done = await Task.WhenAny(Task.WhenAll(clearPooledTask, clearImplicitTask), + Task.Delay(TimeSpan.FromSeconds(2))); + Assert.True(done != Task.Delay(TimeSpan.FromSeconds(2)), "ClearPool() must not block."); await using (var checkPooled = new YdbConnection(csPooled)) { diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs index 0f3f2c8c..c64f6a28 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/YdbImplictConnectionTests.cs @@ -1,6 +1,6 @@ using Xunit; -namespace Ydb.Sdk.Ado.Tests.Session; +namespace Ydb.Sdk.Ado.Tests; public class YdbImplictConnectionTests : TestBase { From e748a85bf7e50e2203c7e6f3afa89a1c7d79f0fa Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 17 Sep 2025 17:35:58 +0300 Subject: [PATCH 42/48] fix lint --- src/Ydb.Sdk/src/Ado/PoolManager.cs | 13 ++++++++-- src/Ydb.Sdk/src/Ado/YdbDataSource.cs | 7 ++++-- .../Session/YdbImplicitStressTests.cs | 24 +++++++------------ 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/PoolManager.cs b/src/Ydb.Sdk/src/Ado/PoolManager.cs index 457df53e..789af937 100644 --- a/src/Ydb.Sdk/src/Ado/PoolManager.cs +++ b/src/Ydb.Sdk/src/Ado/PoolManager.cs @@ -36,8 +36,17 @@ CancellationToken cancellationToken driver.RegisterOwner(); - var factory = new PoolingSessionFactory(driver, settings); - var newSessionPool = new PoolingSessionSource(factory, settings); + ISessionSource newSessionPool; + + if (settings.MaxSessionPool > 0) + { + var factory = new PoolingSessionFactory(driver, settings); + newSessionPool = new PoolingSessionSource(factory, settings); + } + else + { + newSessionPool = new ImplicitSessionSource(driver); + } Pools[settings.ConnectionString] = newSessionPool; diff --git a/src/Ydb.Sdk/src/Ado/YdbDataSource.cs b/src/Ydb.Sdk/src/Ado/YdbDataSource.cs index a4fdf0c9..d37e94c3 100644 --- a/src/Ydb.Sdk/src/Ado/YdbDataSource.cs +++ b/src/Ydb.Sdk/src/Ado/YdbDataSource.cs @@ -391,6 +391,7 @@ public async ValueTask OpenRetryableConnectionAsync(CancellationT try { await ydbConnection.OpenAsync(_retryPolicyExecutor, cancellationToken); + return ydbConnection; } catch @@ -408,7 +409,8 @@ public async ValueTask OpenRetryableConnectionAsync( var ydbConnection = CreateDbConnection(); try { - await ydbConnection.OpenAsync(_retryPolicyExecutor, cancellationToken); + await ydbConnection.OpenAsync(GetExecutor(ydbRetryPolicyConfig), cancellationToken); + return ydbConnection; } catch @@ -426,7 +428,8 @@ public async ValueTask OpenRetryableConnectionAsync( var ydbConnection = CreateDbConnection(); try { - await ydbConnection.OpenAsync(_retryPolicyExecutor, cancellationToken); + await ydbConnection.OpenAsync(new YdbRetryPolicyExecutor(retryPolicy), cancellationToken); + return ydbConnection; } catch diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs index 4a4ed6c4..3c5e840b 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs @@ -29,8 +29,6 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() var source = new ImplicitSessionSource(driver); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var workers = Enumerable.Range(0, 200).Select(async _ => { var rnd = Random.Shared; @@ -38,10 +36,10 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() { try { - var s = await source.OpenSession(cts.Token); + var s = await source.OpenSession(CancellationToken.None); opened.Inc(); - await Task.Delay(rnd.Next(0, 5), cts.Token); + await Task.Delay(rnd.Next(0, 5)); s.Dispose(); closed.Inc(); @@ -54,9 +52,9 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() var disposer = Task.Run(async () => { - await Task.Delay(10, cts.Token); + await Task.Delay(10); await source.DisposeAsync(); - }, cts.Token); + }); await Task.WhenAll(workers.Append(disposer)); @@ -76,8 +74,6 @@ public async Task Stress_Counts_AreBalanced() var source = new ImplicitSessionSource(driver); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var workers = Enumerable.Range(0, 200).Select(async _ => { var rnd = Random.Shared; @@ -85,10 +81,10 @@ public async Task Stress_Counts_AreBalanced() { try { - var s = await source.OpenSession(cts.Token); + var s = await source.OpenSession(CancellationToken.None); opened.Inc(); - await Task.Delay(rnd.Next(0, 3), cts.Token); + await Task.Delay(rnd.Next(0, 3)); s.Dispose(); closed.Inc(); @@ -99,7 +95,7 @@ public async Task Stress_Counts_AreBalanced() } }).ToArray(); - var disposer = Task.Run(async () => await source.DisposeAsync(), cts.Token); + var disposer = Task.Run(async () => await source.DisposeAsync()); await Task.WhenAll(workers.Append(disposer)); @@ -116,13 +112,11 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() var source = new ImplicitSessionSource(driver); - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var opens = Enumerable.Range(0, 1000).Select(async _ => { try { - var s = await source.OpenSession(cts.Token); + var s = await source.OpenSession(CancellationToken.None); s.Dispose(); return 1; } @@ -136,7 +130,7 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() { await Task.Yield(); await source.DisposeAsync(); - }, cts.Token); + }); await Task.WhenAll(opens.Append(disposeTask)); From 08e6bc2a509b456ea30792b861e0507a7d7b2c97 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Thu, 18 Sep 2025 21:27:38 +0300 Subject: [PATCH 43/48] tests: add double OpenSession check in stress tests; --- src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs | 2 +- .../Session/YdbImplicitStressTests.cs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index f094c518..452673ed 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -3,7 +3,7 @@ namespace Ydb.Sdk.Ado.Session; internal sealed class ImplicitSessionSource : ISessionSource { private readonly IDriver _driver; - private readonly ManualResetEventSlim _allReleased = new(false); + private readonly ManualResetEventSlim _allReleased = new(initialState: false); private int _isDisposed; private int _activeLeaseCount; diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs index 3c5e840b..dc16d998 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs @@ -43,6 +43,9 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() s.Dispose(); closed.Inc(); + + var s2 = await source.OpenSession(CancellationToken.None); + s2.Dispose(); } catch (ObjectDisposedException) { @@ -88,6 +91,9 @@ public async Task Stress_Counts_AreBalanced() s.Dispose(); closed.Inc(); + + var s2 = await source.OpenSession(CancellationToken.None); + s2.Dispose(); } catch (ObjectDisposedException) { @@ -118,6 +124,10 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() { var s = await source.OpenSession(CancellationToken.None); s.Dispose(); + + var s2 = await source.OpenSession(CancellationToken.None); + s2.Dispose(); + return 1; } catch (ObjectDisposedException) From 448244f0dbb061cf444a586235d992a85c163973 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Thu, 18 Sep 2025 21:35:27 +0300 Subject: [PATCH 44/48] try fix --- src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 452673ed..f094c518 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -3,7 +3,7 @@ namespace Ydb.Sdk.Ado.Session; internal sealed class ImplicitSessionSource : ISessionSource { private readonly IDriver _driver; - private readonly ManualResetEventSlim _allReleased = new(initialState: false); + private readonly ManualResetEventSlim _allReleased = new(false); private int _isDisposed; private int _activeLeaseCount; From e001a795cb913801742b5560ca88f26b2edad432 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Fri, 19 Sep 2025 19:18:57 +0300 Subject: [PATCH 45/48] test --- .../Session/YdbImplicitStressTests.cs | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs index dc16d998..f837a229 100644 --- a/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs +++ b/src/Ydb.Sdk/test/Ydb.Sdk.Ado.Tests/Session/YdbImplicitStressTests.cs @@ -34,22 +34,24 @@ public async Task Dispose_WaitsForAllLeases_AndSignalsOnEmptyExactlyOnce() var rnd = Random.Shared; for (var j = 0; j < 10; j++) { + ISession s; try { - var s = await source.OpenSession(CancellationToken.None); + s = await source.OpenSession(CancellationToken.None); opened.Inc(); await Task.Delay(rnd.Next(0, 5)); - - s.Dispose(); - closed.Inc(); - - var s2 = await source.OpenSession(CancellationToken.None); - s2.Dispose(); } catch (ObjectDisposedException) { + return; } + + var s2 = await source.OpenSession(CancellationToken.None); + s2.Dispose(); + + s.Dispose(); + closed.Inc(); } }).ToArray(); @@ -82,22 +84,24 @@ public async Task Stress_Counts_AreBalanced() var rnd = Random.Shared; for (var j = 0; j < 10; j++) { + ISession s; try { - var s = await source.OpenSession(CancellationToken.None); + s = await source.OpenSession(CancellationToken.None); opened.Inc(); await Task.Delay(rnd.Next(0, 3)); - - s.Dispose(); - closed.Inc(); - - var s2 = await source.OpenSession(CancellationToken.None); - s2.Dispose(); } catch (ObjectDisposedException) { + return; } + + var s2 = await source.OpenSession(CancellationToken.None); + s2.Dispose(); + + s.Dispose(); + closed.Inc(); } }).ToArray(); @@ -120,20 +124,21 @@ public async Task Open_RacingWithDispose_StateRemainsConsistent() var opens = Enumerable.Range(0, 1000).Select(async _ => { + ISession s; try { - var s = await source.OpenSession(CancellationToken.None); - s.Dispose(); - - var s2 = await source.OpenSession(CancellationToken.None); - s2.Dispose(); - - return 1; + s = await source.OpenSession(CancellationToken.None); } catch (ObjectDisposedException) { return 0; } + + var s2 = await source.OpenSession(CancellationToken.None); + s2.Dispose(); + + s.Dispose(); + return 1; }).ToArray(); var disposeTask = Task.Run(async () => From ff61b60922413f194a1d98088de247efbd3424c3 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 24 Sep 2025 03:48:49 +0300 Subject: [PATCH 46/48] made dispose two-phase --- src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index f094c518..3b81b4d5 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -4,7 +4,8 @@ internal sealed class ImplicitSessionSource : ISessionSource { private readonly IDriver _driver; private readonly ManualResetEventSlim _allReleased = new(false); - private int _isDisposed; + + private int _state; private int _activeLeaseCount; internal ImplicitSessionSource(IDriver driver) @@ -24,12 +25,12 @@ public ValueTask OpenSession(CancellationToken cancellationToken) private bool TryAcquireLease() { - if (Volatile.Read(ref _isDisposed) != 0) + if (Volatile.Read(ref _state) == 2) return false; Interlocked.Increment(ref _activeLeaseCount); - if (Volatile.Read(ref _isDisposed) != 0) + if (Volatile.Read(ref _state) == 2) { Interlocked.Decrement(ref _activeLeaseCount); return false; @@ -40,13 +41,13 @@ private bool TryAcquireLease() internal void ReleaseLease() { - if (Interlocked.Decrement(ref _activeLeaseCount) == 0 && Volatile.Read(ref _isDisposed) != 0) + if (Interlocked.Decrement(ref _activeLeaseCount) == 0 && Volatile.Read(ref _state) == 1) _allReleased.Set(); } public async ValueTask DisposeAsync() { - if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) + if (Interlocked.CompareExchange(ref _state, 1, 0) != 0) return; if (Volatile.Read(ref _activeLeaseCount) != 0) @@ -54,6 +55,7 @@ public async ValueTask DisposeAsync() try { + Volatile.Write(ref _state, 2); await _driver.DisposeAsync(); } finally From 6852546945bb5dc8e3d039f00f3014c62602dffc Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 24 Sep 2025 04:11:24 +0300 Subject: [PATCH 47/48] =?UTF-8?q?tried=20to=20make=20a=20=E2=80=9Ctwo-phas?= =?UTF-8?q?e=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Ado/Session/ImplicitSessionSource.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 3b81b4d5..703fabba 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -2,6 +2,8 @@ namespace Ydb.Sdk.Ado.Session; internal sealed class ImplicitSessionSource : ISessionSource { + private enum State { Open = 0, Closing = 1, Closed = 2 } + private readonly IDriver _driver; private readonly ManualResetEventSlim _allReleased = new(false); @@ -25,12 +27,12 @@ public ValueTask OpenSession(CancellationToken cancellationToken) private bool TryAcquireLease() { - if (Volatile.Read(ref _state) == 2) + if (Volatile.Read(ref _state) == (int)State.Closed) return false; Interlocked.Increment(ref _activeLeaseCount); - if (Volatile.Read(ref _state) == 2) + if (Volatile.Read(ref _state) == (int)State.Closed) { Interlocked.Decrement(ref _activeLeaseCount); return false; @@ -41,21 +43,30 @@ private bool TryAcquireLease() internal void ReleaseLease() { - if (Interlocked.Decrement(ref _activeLeaseCount) == 0 && Volatile.Read(ref _state) == 1) + if (Interlocked.Decrement(ref _activeLeaseCount) == 0 && + Volatile.Read(ref _state) != (int)State.Open) + { _allReleased.Set(); + } } public async ValueTask DisposeAsync() { - if (Interlocked.CompareExchange(ref _state, 1, 0) != 0) - return; + var prev = Interlocked.CompareExchange(ref _state, (int)State.Closing, (int)State.Open); + switch (prev) + { + case (int)State.Closed: + return; + case (int)State.Closing: + break; + } if (Volatile.Read(ref _activeLeaseCount) != 0) _allReleased.Wait(); try { - Volatile.Write(ref _state, 2); + Volatile.Write(ref _state, (int)State.Closed); await _driver.DisposeAsync(); } finally From 401e7d21a921af2e308f744a3cdcf9adae731080 Mon Sep 17 00:00:00 2001 From: bogdangalka Date: Wed, 24 Sep 2025 11:14:33 +0300 Subject: [PATCH 48/48] try --- .../src/Ado/Session/ImplicitSessionSource.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs index 703fabba..b40b93e2 100644 --- a/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs +++ b/src/Ydb.Sdk/src/Ado/Session/ImplicitSessionSource.cs @@ -2,8 +2,6 @@ namespace Ydb.Sdk.Ado.Session; internal sealed class ImplicitSessionSource : ISessionSource { - private enum State { Open = 0, Closing = 1, Closed = 2 } - private readonly IDriver _driver; private readonly ManualResetEventSlim _allReleased = new(false); @@ -27,12 +25,14 @@ public ValueTask OpenSession(CancellationToken cancellationToken) private bool TryAcquireLease() { - if (Volatile.Read(ref _state) == (int)State.Closed) + if (Volatile.Read(ref _state) == 2) return false; - Interlocked.Increment(ref _activeLeaseCount); + var newCount = Interlocked.Increment(ref _activeLeaseCount); + + var state = Volatile.Read(ref _state); - if (Volatile.Read(ref _state) == (int)State.Closed) + if (state == 2 || (state == 1 && newCount == 1)) { Interlocked.Decrement(ref _activeLeaseCount); return false; @@ -44,7 +44,7 @@ private bool TryAcquireLease() internal void ReleaseLease() { if (Interlocked.Decrement(ref _activeLeaseCount) == 0 && - Volatile.Read(ref _state) != (int)State.Open) + Volatile.Read(ref _state) != 0) { _allReleased.Set(); } @@ -52,21 +52,15 @@ internal void ReleaseLease() public async ValueTask DisposeAsync() { - var prev = Interlocked.CompareExchange(ref _state, (int)State.Closing, (int)State.Open); - switch (prev) - { - case (int)State.Closed: - return; - case (int)State.Closing: - break; - } + if (Interlocked.CompareExchange(ref _state, 1, 0) != 0) + return; if (Volatile.Read(ref _activeLeaseCount) != 0) _allReleased.Wait(); try { - Volatile.Write(ref _state, (int)State.Closed); + Volatile.Write(ref _state, 2); await _driver.DisposeAsync(); } finally