Skip to content

Reset session isolation level on pool return (fixes #96)#4330

Draft
priyankatiwari08 wants to merge 3 commits into
dotnet:mainfrom
priyankatiwari08:feature/sqltransaction-isolation-leak
Draft

Reset session isolation level on pool return (fixes #96)#4330
priyankatiwari08 wants to merge 3 commits into
dotnet:mainfrom
priyankatiwari08:feature/sqltransaction-isolation-leak

Conversation

@priyankatiwari08
Copy link
Copy Markdown
Contributor

Fixes #96.

Problem

SqlTransaction and TransactionScope leave the underlying SQL Server
session with the elevated isolation level after Commit/Rollback. Because
sp_reset_connection does not reset the session isolation level, the next
user of the pooled physical connection silently inherits it.

Repro

  • using var c = new SqlConnection(cs); c.Open();
  • using var tx = c.BeginTransaction(IsolationLevel.Serializable); tx.Rollback();
  • Dispose, reopen on the same pooled SPID — sys.dm_exec_sessions.transaction_isolation_level is still 4 (Serializable).

Reproduced on 5.2.2 and 7.0.1 against SQL Server, on both net8.0 and net472.

Fix

  • Track in SqlInternalConnectionTds when a non-default isolation level was set via the TM Begin path (_isolationLevelDirty).
  • In ResetConnection() (called from Deactivate on pool return), if dirty, issue SET TRANSACTION ISOLATION LEVEL READ COMMITTED; via TdsExecuteSQLBatch. The batch piggybacks the queued sp_reset_connection byte on the same TDS header, so there is no extra round trip vs. the legacy reset.
  • On failure, the connection is doomed so it's destroyed instead of pooled.

Kill switch

Switch.Microsoft.Data.SqlClient.UseLegacyIsolationLevelBehavior (default false) restores the previous behavior.

Tests

Adds IsolationLevelLeakTest under ManualTests/SQL/TransactionTest/:

  • SqlTransaction_SerializableDoesNotLeakAcrossPool
  • TransactionScope_SerializableDoesNotLeakAcrossPool
  • LegacySwitch_PreservesOldLeakBehavior (negative test)

Validation

Built clean for net462 / net8.0 / net9.0. Standalone repro run end-to-end against local SQL Server: [After] isolation is now ReadCommitted on both scenarios with the same pooled SPID. With the legacy switch enabled, [After] is Serializable (kill-switch verified).

Copilot AI review requested due to automatic review settings June 1, 2026 13:59
@priyankatiwari08 priyankatiwari08 requested a review from a team as a code owner June 1, 2026 13:59
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Jun 1, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

SqlTransaction and TransactionScope leave the underlying SQL Server session with the elevated isolation level after Commit/Rollback. sp_reset_connection does not reset it, so the next user of the pooled physical connection silently inherits it.

This change tracks when a non-default isolation level was set via the TM Begin path and, on pool return, issues a 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED' batch right before sp_reset_connection (piggybacked on the same TDS header, no extra round trip). On failure the connection is doomed instead of returned to the pool.

A new AppContext switch 'Switch.Microsoft.Data.SqlClient.UseLegacyIsolationLevelBehavior' preserves previous behavior for callers that depend on it (default: false).

Adds IsolationLevelLeakTest under ManualTests covering the two repro scenarios plus a negative test for the kill switch.
@priyankatiwari08 priyankatiwari08 force-pushed the feature/sqltransaction-isolation-leak branch from 4e8ed47 to 9f67fb1 Compare June 1, 2026 14:19
@priyankatiwari08 priyankatiwari08 requested a review from Copilot June 1, 2026 14:20
@priyankatiwari08 priyankatiwari08 added this to the 7.0.2 milestone Jun 1, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment on lines +31 to +38
private static string PooledMaxOneConnString =>
new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString)
{
Pooling = true,
MaxPoolSize = 1,
MultipleActiveResultSets = false,
ApplicationName = "IsoLeakTest"
}.ConnectionString;
Comment on lines +106 to +109
const string Switch = "Switch.Microsoft.Data.SqlClient.UseLegacyIsolationLevelBehavior";
AppContext.SetSwitch(Switch, true);
try
{
Comment on lines +136 to +140
finally
{
AppContext.SetSwitch(Switch, false);
SqlConnection.ClearAllPools();
}
Copy link
Copy Markdown
Contributor

@apoorvdeshmukh apoorvdeshmukh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you look into this test failure?
Seems related to the changes

- IsolationLevelLeakTest: switch to LocalAppContextSwitchesHelper so the cached LocalAppContextSwitches value is forced, not just set via AppContext.SetSwitch (production code reads the cache after first use, which made the negative test order-dependent and caused the CI failure).

- Helper restores the previous value on dispose, so global state does not leak to other tests.

- TransactionScope test: set Enlist=true explicitly on the pooled connection string so the test does not depend on the user's local DataTestUtility.TCPConnectionString defaults.

- LocalAppContextSwitchesHelper: expose UseLegacyIsolationLevelBehavior get/set + capture/restore wired through the standard reflection helpers.
@priyankatiwari08 priyankatiwari08 moved this from Waiting for customer to In progress in SqlClient Board Jun 3, 2026
Copy link
Copy Markdown
Contributor

@apoorvdeshmukh apoorvdeshmukh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you look into this failure for LegacySwitch_PreservesOldLeakBehavior?
Tests are mostly failing with this one. You can put the PR into draft till this is addressed.
Is this happening only in CI or do you observe it locally under VS as well?

@github-project-automation github-project-automation Bot moved this from In progress to Waiting for customer in SqlClient Board Jun 3, 2026
@apoorvdeshmukh apoorvdeshmukh added the Author attention needed PRs that require author to respond or make updates to PR. label Jun 3, 2026
The leak only manifests on on-prem SQL Server: Azure SQL DB resets the session isolation level inside sp_reset_connection, so the legacy switch becomes a no-op there and the assertion (expects 'Serializable' to leak) fails on Azure CI legs.
Copilot AI review requested due to automatic review settings June 3, 2026 11:45
@priyankatiwari08 priyankatiwari08 marked this pull request as draft June 3, 2026 11:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment on lines +5 to +9
using System;
using System.Data;
using System.Transactions;
using Microsoft.Data.SqlClient.Tests.Common;
using Xunit;
Comment on lines +462 to +476
/// <summary>
/// When set to true, pooled connections preserve the legacy behavior where
/// the SQL Server session transaction isolation level is not reset on
/// return to the pool. As a result, the next user of the pooled connection
/// may inherit a non-default isolation level.
///
/// The default value of this switch is false, meaning the driver will
/// reset the isolation level to READ COMMITTED before the next checkout
/// when it knows the level was changed.
/// </summary>
public static bool UseLegacyIsolationLevelBehavior =>
AcquireAndReturn(
UseLegacyIsolationLevelBehaviorString,
defaultValue: false,
ref s_useLegacyIsolationLevelBehavior);
isoLevel != TdsEnums.TransactionManagerIsolationLevel.Unspecified &&
isoLevel != TdsEnums.TransactionManagerIsolationLevel.ReadCommitted)
{
_isolationLevelDirty = true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth noting that Azure Synapse Analytics does things slightly differently - it only supports the SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED statement, and the default isolation level can vary based upon a database option.

I think this might also present an issue for #4335.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author attention needed PRs that require author to respond or make updates to PR.

Projects

Status: Waiting for customer

Development

Successfully merging this pull request may close these issues.

SqlTransaction and TransactionScope leak isolation level

4 participants