From 70a31f4727a941479ec273d81c5db3a1c3175fe4 Mon Sep 17 00:00:00 2001 From: "Dailey, Alaric" Date: Thu, 29 Oct 2026 02:12:59 -0500 Subject: [PATCH] fix(spanner): clear transaction connection after completion --- .../SpannerTransactionTests.cs | 49 +++++++++++++++++++ .../SpannerTransaction.cs | 5 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerTransactionTests.cs b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerTransactionTests.cs index c9638e557cf2..1911a91ae320 100644 --- a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerTransactionTests.cs +++ b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data.Tests/SpannerTransactionTests.cs @@ -16,6 +16,7 @@ using Google.Cloud.Spanner.V1.Internal.Logging; using Google.Cloud.Spanner.V1.Tests; using System; +using System.Threading.Tasks; using Xunit; namespace Google.Cloud.Spanner.Data.Tests; @@ -47,6 +48,54 @@ public void MaxCommitDelay_DefaultsToNull() Assert.Null(transaction.TransactionOptions.MaxCommitDelay); } + [Fact] + public void Connection_IsNullAfterDispose() + { + SpannerClient spannerClientMock = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger); + spannerClientMock.SetupCreateSessionAsync(); + SpannerConnection connection = SpannerCommandTests.BuildSpannerConnection(spannerClientMock); + SpannerTransaction transaction = connection.BeginTransaction(); + + Assert.Same(connection, transaction.Connection); + + transaction.Dispose(); + + Assert.Null(transaction.Connection); + } + + [Fact] + public async Task Connection_IsNullAfterCommit() + { + SpannerClient spannerClientMock = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger); + spannerClientMock.SetupCreateSessionAsync().SetupExecuteStreamingSql().SetupCommitAsync(); + SpannerConnection connection = SpannerCommandTests.BuildSpannerConnection(spannerClientMock); + SpannerTransaction transaction = connection.BeginTransaction(); + + using (var command = connection.CreateSelectCommand("SELECT 1")) + { + command.Transaction = transaction; + using var reader = await command.ExecuteReaderAsync(); + Assert.True(reader.HasRows); + } + + await transaction.CommitAsync(); + + Assert.Null(transaction.Connection); + } + + [Fact] + public async Task Connection_IsNullAfterRollback() + { + SpannerClient spannerClientMock = SpannerClientHelpers.CreateMockClient(Logger.DefaultLogger); + spannerClientMock.SetupCreateSessionAsync().SetupRollbackAsync(); + SpannerConnection connection = SpannerCommandTests.BuildSpannerConnection(spannerClientMock); + SpannerTransaction transaction = connection.BeginTransaction(); + + await transaction.RollbackAsync(); + + Assert.Null(transaction.Connection); + } + [Theory, MemberData(nameof(ValidMaxCommitDelayValues))] public void MaxCommitDelay_Valid(TimeSpan? maxCommitDelay) { diff --git a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerTransaction.cs b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerTransaction.cs index e47a12fdc9c4..76c3320a87e7 100644 --- a/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerTransaction.cs +++ b/apis/Google.Cloud.Spanner.Data/Google.Cloud.Spanner.Data/SpannerTransaction.cs @@ -64,6 +64,7 @@ public sealed class SpannerTransaction : SpannerTransactionBase, ISpannerTransac private readonly bool _isRetriable = false; private int _disposed = 0; private int _commited = 0; + private int _completed = 0; // Flag indicating whether the transaction has executed at least one statement. // The TransactionTag may no longer be set once the transaction has executed one @@ -153,7 +154,7 @@ public int CommitTimeout public TimestampBound TimestampBound => _creationOptions.TimestampBound ?? _creationOptions.TransactionId?.TimestampBound; /// - protected override DbConnection DbConnection => SpannerConnection; + protected override DbConnection DbConnection => _disposed == 0 && _completed == 0 ? SpannerConnection : null; /// /// associated with this transaction. @@ -448,6 +449,7 @@ Task> ISpannerTransaction.ExecuteBatchDmlAsync(ExecuteBatchDml settings => settings.CommitSettings, TransactionOptions.EffectiveCommitTimeout(SpannerConnection), cancellationToken); var response = await _session.CommitAsync(request, callSettings).ConfigureAwait(false); Interlocked.Exchange(ref _commited, 1); + Interlocked.Exchange(ref _completed, 1); // We dispose of the SpannerTransaction to inmediately release the session to the pool when possible. Dispose(); if (response.CommitTimestamp == null) @@ -503,6 +505,7 @@ public override async Task RollbackAsync(CancellationToken cancellationToken = d await ExecuteHelper.WithErrorTranslationAndProfiling( () => _session.RollbackAsync(new RollbackRequest(), callSettings), "SpannerTransaction.Rollback", SpannerConnection.Logger).ConfigureAwait(false); + Interlocked.Exchange(ref _completed, 1); Dispose(); }