Re-issue session isolation level on TransactionScope re-enlistment (fixes #146)#4335
Draft
priyankatiwari08 wants to merge 2 commits into
Draft
Conversation
When a pooled connection is re-checked-out inside the same System.Transactions transaction, the existing Enlist() short-circuit skipped re-issuing SET TRANSACTION ISOLATION LEVEL. sp_reset_connection_keep_transaction resets the session isolation level to the database default on Azure SQL DB, silently downgrading subsequent commands in the scope (e.g. Serializable -> Read Committed Snapshot). Fix: on the re-attach path, re-issue SET TRANSACTION ISOLATION LEVEL matching the ambient transaction's isolation level. The statement is queued onto the same TDS batch as the pending reset, so there is no extra round trip. Back-compat: gated behind AppContext switch Switch.Microsoft.Data.SqlClient.UseLegacyTransactionScopeIsolationBehavior (default false). Validated against on-prem SQL Server (no behavior change) and Azure SQL DB (downgrade gone). Adds ManualTests gated on IsAzureServer.
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes an Azure SQL DB-specific TransactionScope pooling regression where session isolation level can revert to the database default after transacted-pool re-checkout by re-asserting the ambient isolation level during re-enlistment.
Changes:
- Added a new AppContext switch (
Switch.Microsoft.Data.SqlClient.UseLegacyTransactionScopeIsolationBehavior) to gate the new re-assert behavior. - Updated
SqlInternalConnectionTds.Enlist(Transaction)to re-issueSET TRANSACTION ISOLATION LEVEL ...on the “same transaction” short-circuit path (intended to piggyback on the pending reset). - Added new Azure-gated ManualTests and wired them into the ManualTests project.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs | Adds re-attach logic to re-assert session isolation level when re-enlisting into the same ambient transaction. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs | Introduces a new AppContext switch to enable legacy behavior. |
| src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionScopeIsolationReassertTest.cs | Adds ManualTests validating isolation level stability across pooled re-opens inside a TransactionScope (Azure-only). |
| src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj | Includes the new ManualTests source file in the build. |
Comment on lines
+2426
to
+2431
| Task executeTask = _parser.TdsExecuteSQLBatch( | ||
| $"SET TRANSACTION ISOLATION LEVEL {isoSql};", | ||
| timeout: 0, | ||
| notificationRequest: null, | ||
| _parser._physicalStateObj, | ||
| sync: true); |
Comment on lines
213
to
+220
| /// </summary> | ||
| private static SwitchValue s_useLegacyFailoverAlternationOnLoginSqlErrors = SwitchValue.None; | ||
|
|
||
| /// <summary> | ||
| /// The cached value of the UseLegacyTransactionScopeIsolationBehavior switch. | ||
| /// </summary> | ||
| private static SwitchValue s_useLegacyTransactionScopeIsolationBehavior = SwitchValue.None; | ||
|
|
Comment on lines
+33
to
+59
| [ConditionalFact( | ||
| typeof(DataTestUtility), | ||
| nameof(DataTestUtility.AreConnStringsSetup), | ||
| nameof(DataTestUtility.IsAzureServer))] | ||
| public static async Task TransactionScope_SerializableHonoredAcrossPoolReuse() | ||
| { | ||
| string cs = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) | ||
| { | ||
| Pooling = true, | ||
| MaxPoolSize = 1, | ||
| ApplicationName = nameof(TransactionScopeIsolationReassertTest) | ||
| }.ConnectionString; | ||
|
|
||
| using (var scope = new TransactionScope( | ||
| TransactionScopeOption.Required, | ||
| new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.Serializable }, | ||
| TransactionScopeAsyncFlowOption.Enabled)) | ||
| { | ||
| for (int i = 0; i < 3; i++) | ||
| { | ||
| string level = await GetSessionIsolationLevelAsync(cs); | ||
| Assert.Equal("Serializable", level); | ||
| } | ||
|
|
||
| scope.Complete(); | ||
| } | ||
| } |
Comment on lines
+2381
to
+2383
| else if (!LocalAppContextSwitches.UseLegacyTransactionScopeIsolationBehavior | ||
| && _fResetConnection) | ||
| { |
- SqlConnectionInternal.Enlist: guard on _parser._fResetConnection (runtime reset-pending flag) instead of _fResetConnection (static config). - ReassertSessionIsolationLevel: use ConnectionOptions.ConnectTimeout for the in-driver SET batch (matches ChangeDatabase convention) instead of timeout: 0. - LocalAppContextSwitchesHelper / LocalAppContextSwitchesTest: wire UseLegacyTransactionScopeIsolationBehavior into the RAII helper and the defaults test. - ManualTests: add LegacySwitch_PreservesAzureDowngradeBehavior negative test asserting the back-compat switch fully restores the prior Azure downgrade behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix #146 —
TransactionScopeambient isolation level is silently downgraded after the first pooled connection re-checkout on Azure SQL DB.Repro
On on-prem SQL Server the level survives and all three opens report
serializable. On Azure SQL DB the second and subsequent opens report the database default isolation (e.g.read committed snapshot).Root cause
Open()inside the scope enlists the connection and sendsSET TRANSACTION ISOLATION LEVEL <ambient>;.Close()the physical connection is returned to the transacted pool still enlisted in the sameTransaction.Open()reuses the same physical connection.SqlInternalConnectionTds.Enlist(Transaction)seestransaction.Equals(EnlistedTransaction)and short-circuits — no SET is sent.sp_reset_connection_keep_transactionpiggybacks on the next batch. On Azure SQL DB this reset clears the session isolation level to the database default; on on-prem SQL Server it does not, which is why the bug is Azure-only in practice.Fix
On the short-circuit path in
Enlist, when reset is pending and the new behavior switch is enabled, re-issue:mapped from
Transaction.IsolationLevel. The statement is queued onto the same TDS batch as the reset, so there is no extra round trip.Gated behind a new AppContext switch for back-compat:
Switch.Microsoft.Data.SqlClient.UseLegacyTransactionScopeIsolationBehavior(defaultfalse).Same back-compat pattern used by the related companion PR #4330 (
UseLegacyIsolationLevelBehavior).Files changed
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalAppContextSwitches.cs— new switch.src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs— new re-attach branch +ReassertSessionIsolationLevelhelper.src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/TransactionTest/TransactionScopeIsolationReassertTest.cs— new ManualTests, gated onIsAzureServer.src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj— wire-up.Validation
End-to-end repro against both back ends with
Pooling=true; Max Pool Size=1, opening 3 connections per scope.Build clean on
net462,net8.0,net9.0(0 warnings, 0 errors).Notes
DataTestUtility.IsAzureServerbecause the bug does not manifest against on-prem SQL Server.