From 8c66406e4bdd31c34a48b6ecf7280a5f996149dc Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Mon, 10 Nov 2025 10:04:17 -0800 Subject: [PATCH 01/18] Fix test failures. --- ...leDbConnectionPoolTransactionStressTest.cs | 1412 +++++++++++++++++ 1 file changed, 1412 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs new file mode 100644 index 0000000000..fb8ae57212 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -0,0 +1,1412 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; +using Microsoft.Data.Common; +using Microsoft.Data.Common.ConnectionString; +using Microsoft.Data.ProviderBase; +using Microsoft.Data.SqlClient.ConnectionPool; +using Xunit; + +#nullable enable + +namespace Microsoft.Data.SqlClient.UnitTests.ConnectionPool; + +/// +/// Stress tests for WaitHandleDbConnectionPool transaction functionality under high concurrency and load. +/// These tests verify that pool metrics remain consistent when connections are rapidly opened and closed +/// with intermingled transactions in a highly concurrent environment. +/// +public class WaitHandleDbConnectionPoolTransactionStressTest +{ + private const int DefaultMaxPoolSize = 50; + private const int DefaultMinPoolSize = 0; + private const int DefaultCreationTimeout = 15; + + // Thread-safe random number generator for .NET Framework compatibility + private static readonly ThreadLocal s_random = new ThreadLocal(() => new Random(Guid.NewGuid().GetHashCode())); + + #region Helper Methods + + private WaitHandleDbConnectionPool CreatePool( + int maxPoolSize = DefaultMaxPoolSize, + int minPoolSize = DefaultMinPoolSize, + bool hasTransactionAffinity = true) + { + var poolGroupOptions = new DbConnectionPoolGroupOptions( + poolByIdentity: false, + minPoolSize: minPoolSize, + maxPoolSize: maxPoolSize, + creationTimeout: DefaultCreationTimeout, + loadBalanceTimeout: 0, + hasTransactionAffinity: hasTransactionAffinity + ); + + var dbConnectionPoolGroup = new DbConnectionPoolGroup( + new DbConnectionOptions("DataSource=localhost;", null), + new DbConnectionPoolKey("TestDataSource"), + poolGroupOptions + ); + + var connectionFactory = new MockSqlConnectionFactory(); + + var pool = new WaitHandleDbConnectionPool( + connectionFactory, + dbConnectionPoolGroup, + DbConnectionPoolIdentity.NoIdentity, + new DbConnectionPoolProviderInfo() + ); + + pool.Startup(); + return pool; + } + + private void AssertPoolMetrics(WaitHandleDbConnectionPool pool, int expectedMaxCount, string context) + { + Assert.True(pool.Count <= expectedMaxCount, + $"{context}: Pool count ({pool.Count}) exceeded max pool size ({expectedMaxCount})"); + Assert.True(pool.Count >= 0, + $"{context}: Pool count ({pool.Count}) is negative"); + } + + #endregion + + #region Basic Transaction Stress Tests + + [Fact] + public void StressTest_RapidTransactionOpenClose_SingleThreaded() + { + // Arrange + var pool = CreatePool(maxPoolSize: 10); + const int iterations = 1000; + var connections = new List(); + + try + { + // Act + for (int i = 0; i < iterations; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + var obtained = pool.TryGetConnection( + owner, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? connection); + + Assert.True(obtained); + Assert.NotNull(connection); + connections.Add(connection); + + pool.ReturnInternalConnection(connection, owner); + scope.Complete(); + } + + // Assert + AssertPoolMetrics(pool, 10, "After rapid single-threaded transactions"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_ConcurrentTransactions_MultipleThreads() + { + // Arrange + var pool = CreatePool(maxPoolSize: 20); + const int threadCount = 10; + const int iterationsPerThread = 100; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + var obtained = pool.TryGetConnection( + owner, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? connection); + + Assert.True(obtained); + Assert.NotNull(connection); + + // Simulate some work + Thread.Sleep(1); + + pool.ReturnInternalConnection(connection, owner); + scope.Complete(); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 20, "After concurrent transactions"); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + #region Intermingled Transaction Stress Tests + + [Fact] + public void StressTest_InterminledTransactions_RapidScopeChanges() + { + // Arrange + var pool = CreatePool(maxPoolSize: 30); + const int threadCount = 15; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var totalConnections = new ConcurrentBag(); + var exceptions = new ConcurrentBag(); + + try + { + // Act - Each thread creates nested and sequential transactions + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + // Sequential transactions + using (var scope1 = new TransactionScope()) + { + var owner1 = new SqlConnection(); + pool.TryGetConnection(owner1, null, new DbConnectionOptions("", null), out var conn1); + Assert.NotNull(conn1); + totalConnections.Add(conn1); + pool.ReturnInternalConnection(conn1, owner1); + scope1.Complete(); + } + + using (var scope2 = new TransactionScope()) + { + var owner2 = new SqlConnection(); + pool.TryGetConnection(owner2, null, new DbConnectionOptions("", null), out var conn2); + Assert.NotNull(conn2); + totalConnections.Add(conn2); + pool.ReturnInternalConnection(conn2, owner2); + scope2.Complete(); + } + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 30, "After intermingled transactions"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() + { + // Arrange + var pool = CreatePool(maxPoolSize: 40); + const int threadCount = 20; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act - Half the threads use transactions, half don't + for (int t = 0; t < threadCount; t++) + { + bool useTransactions = t % 2 == 0; + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + if (useTransactions) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + Thread.Sleep(s_random.Value!.Next(1, 5)); + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); + } + else + { + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + Thread.Sleep(s_random.Value!.Next(1, 5)); + pool.ReturnInternalConnection(conn, owner); + } + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 40, "After mixed transacted/non-transacted operations"); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + #region High Load Stress Tests + + [Fact] + public void StressTest_MaxPoolSaturation_WithTransactions() + { + // Arrange + var pool = CreatePool(maxPoolSize: 25); + const int threadCount = 50; // More threads than pool size + const int iterationsPerThread = 20; + var activeTasks = new ConcurrentBag(); + var exceptions = new ConcurrentBag(); + var barrier = new Barrier(threadCount); + + try + { + // Act - Saturate the pool with transactions + var tasks = Enumerable.Range(0, threadCount).Select(t => Task.Run(() => + { + try + { + // Synchronize all threads to start at once + barrier.SignalAndWait(); + + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + var obtained = pool.TryGetConnection( + owner, + null, + new DbConnectionOptions("Timeout=30", null), + out DbConnectionInternal? connection); + + if (obtained && connection != null) + { + // Hold connection briefly + Thread.Sleep(s_random.Value!.Next(1, 10)); + pool.ReturnInternalConnection(connection, owner); + } + + scope.Complete(); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + })).ToArray(); + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 25, "After pool saturation with transactions"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_RapidOpenCloseUnderLoad_ThousandsOfOperations() + { + // Arrange + var pool = CreatePool(maxPoolSize: 30); + const int totalOperations = 5000; + const int maxParallelism = 40; + var exceptions = new ConcurrentBag(); + var completedOperations = 0; + + try + { + // Act - Perform thousands of rapid operations + Parallel.For(0, totalOperations, new ParallelOptions { MaxDegreeOfParallelism = maxParallelism }, i => + { + try + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + var obtained = pool.TryGetConnection( + owner, + null, + new DbConnectionOptions("Timeout=30", null), + out DbConnectionInternal? connection); + + if (obtained && connection != null) + { + pool.ReturnInternalConnection(connection, owner); + Interlocked.Increment(ref completedOperations); + } + + scope.Complete(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + + // Assert + Assert.Empty(exceptions); + Assert.True(completedOperations > 0, "No operations completed successfully"); + AssertPoolMetrics(pool, 30, "After thousands of rapid operations"); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + #region Transaction Affinity Stress Tests + + [Fact] + public void StressTest_TransactionAffinity_ConnectionReuse() + { + // Arrange + var pool = CreatePool(maxPoolSize: 20, hasTransactionAffinity: true); + const int threadCount = 10; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + var connectionReuseCounts = new ConcurrentDictionary(); + + try + { + // Act - Test that connections are properly reused within same transaction + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner1 = new SqlConnection(); + var owner2 = new SqlConnection(); + + // Get first connection + pool.TryGetConnection(owner1, null, new DbConnectionOptions("", null), out var conn1); + Assert.NotNull(conn1); + var conn1Id = conn1!.ObjectID; + + // Return it + pool.ReturnInternalConnection(conn1, owner1); + + // Get second connection in same transaction + pool.TryGetConnection(owner2, null, new DbConnectionOptions("", null), out var conn2); + Assert.NotNull(conn2); + var conn2Id = conn2!.ObjectID; + + // Track if we got the same connection back + if (conn1Id == conn2Id) + { + connectionReuseCounts.AddOrUpdate(Thread.CurrentThread.ManagedThreadId, 1, (k, v) => v + 1); + } + + pool.ReturnInternalConnection(conn2, owner2); + scope.Complete(); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 20, "After transaction affinity test"); + // We should see some connection reuse + Assert.True(connectionReuseCounts.Values.Sum() > 0, "Expected some connection reuse within transactions"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_TransactionIsolation_DifferentTransactionsDifferentConnections() + { + // Arrange + var pool = CreatePool(maxPoolSize: 30); + const int transactionCount = 100; + var tasks = new Task[transactionCount]; + var exceptions = new ConcurrentBag(); + var transactionConnections = new ConcurrentDictionary>(); + + try + { + // Act - Each transaction should be isolated + for (int t = 0; t < transactionCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + using var scope = new TransactionScope(); + var transaction = Transaction.Current; + Assert.NotNull(transaction); + var txId = transaction!.TransactionInformation.LocalIdentifier; + + var connectionIds = new List(); + + // Get multiple connections within same transaction + for (int i = 0; i < 3; i++) + { + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + connectionIds.Add(conn!.ObjectID); + pool.ReturnInternalConnection(conn, owner); + } + + transactionConnections[txId] = connectionIds; + scope.Complete(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.Equal(transactionCount, transactionConnections.Count); + AssertPoolMetrics(pool, 30, "After transaction isolation test"); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + #region Async Transaction Stress Tests + + [Fact] + public async Task StressTest_AsyncTransactions_HighConcurrency() + { + // Arrange + var pool = CreatePool(maxPoolSize: 30); + const int taskCount = 30; // Reduced from 50 + const int iterationsPerTask = 10; // Reduced from 30 + var exceptions = new ConcurrentBag(); + + try + { + // Act + var tasks = Enumerable.Range(0, taskCount).Select(async t => + { + try + { + for (int i = 0; i < iterationsPerTask; i++) + { + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); + var tcs = new TaskCompletionSource(); + + var obtained = pool.TryGetConnection(owner, tcs, new DbConnectionOptions("Timeout=30", null), out var connection); + + if (!obtained) + { + connection = await tcs.Task; + } + + Assert.NotNull(connection); + + // Simulate async work - reduced delay + await Task.Delay(s_random.Value!.Next(1, 3)); + + pool.ReturnInternalConnection(connection!, owner); + scope.Complete(); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }).ToArray(); + + await Task.WhenAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 30, "After async transactions"); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + #region Transaction Completion Stress Tests + + [Fact] + public void StressTest_TransactionRollback_ManyOperations() + { + // Arrange + var pool = CreatePool(maxPoolSize: 20); + const int threadCount = 10; + const int iterationsPerThread = 100; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + var rollbackCount = 0; + + try + { + // Act - Alternate between commit and rollback + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + + // Randomly commit or rollback + if (i % 2 == 0) + { + scope.Complete(); + } + else + { + Interlocked.Increment(ref rollbackCount); + // Don't call Complete - let it rollback + } + + pool.ReturnInternalConnection(conn!, owner); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.True(rollbackCount > 0, "Expected some rollbacks"); + AssertPoolMetrics(pool, 20, "After transaction rollbacks"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_MixedTransactionOutcomes_Concurrent() + { + // Arrange + var pool = CreatePool(maxPoolSize: 30); + const int totalOperations = 1000; + var exceptions = new ConcurrentBag(); + var outcomes = new ConcurrentDictionary(); + + try + { + // Act - Random transaction outcomes + Parallel.For(0, totalOperations, new ParallelOptions { MaxDegreeOfParallelism = 20 }, i => + { + try + { + var outcome = i % 3; // 0=commit, 1=rollback, 2=exception-then-rollback + + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=30", null), out var conn); + Assert.NotNull(conn); + + if (outcome == 0) + { + scope.Complete(); + outcomes.AddOrUpdate("Committed", 1, (k, v) => v + 1); + } + else if (outcome == 1) + { + // Rollback (no Complete call) + outcomes.AddOrUpdate("Rolledback", 1, (k, v) => v + 1); + } + else + { + // Exception then rollback + outcomes.AddOrUpdate("Exception", 1, (k, v) => v + 1); + } + + pool.ReturnInternalConnection(conn!, owner); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 30, "After mixed transaction outcomes"); + Assert.True(outcomes.Values.Sum() > 0, "Expected some transactions to complete"); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + #region Edge Case Stress Tests + + [Fact] + public void StressTest_RapidPoolShutdownDuringTransactions() + { + // Arrange + var pool = CreatePool(maxPoolSize: 15); + const int threadCount = 20; + var barrier = new Barrier(threadCount); + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + barrier.SignalAndWait(); + + for (int i = 0; i < 50; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + var obtained = pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=5", null), out var conn); + + if (obtained && conn != null) + { + pool.ReturnInternalConnection(conn, owner); + } + + scope.Complete(); + } + } + catch (Exception ex) + { + // Some exceptions expected during shutdown + exceptions.Add(ex); + } + }); + } + + // Shutdown pool while operations are in progress + Thread.Sleep(100); + pool.Shutdown(); + + try + { + Task.WaitAll(tasks); + } + catch + { + // Expected - some tasks may fail during shutdown + } + + // Assert - Just verify no crash occurred and pool count is valid + AssertPoolMetrics(pool, 15, "After rapid shutdown during transactions"); + } + + [Fact] + public void StressTest_SingleConnectionPool_HighContention() + { + // Arrange - Pool size of 1 creates maximum contention + var pool = CreatePool(maxPoolSize: 1); + const int threadCount = 20; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + var successCount = 0; + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + var obtained = pool.TryGetConnection( + owner, + null, + new DbConnectionOptions("Timeout=30", null), + out DbConnectionInternal? conn); + + if (obtained && conn != null) + { + Thread.Sleep(1); // Hold briefly + pool.ReturnInternalConnection(conn, owner); + Interlocked.Increment(ref successCount); + } + + scope.Complete(); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.True(successCount > 0, "Expected some successful operations despite high contention"); + AssertPoolMetrics(pool, 1, "After high contention on single connection"); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + #region Transaction Completion Order Tests + + [Fact] + public void StressTest_ReturnBeforeTransactionComplete_ManyOperations() + { + // Arrange - Test returning connection before transaction scope completes + var pool = CreatePool(maxPoolSize: 20); + const int threadCount = 15; + const int iterationsPerThread = 100; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + var successCount = 0; + + try + { + // Act - Return connection before calling scope.Complete() + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + + // Return connection BEFORE completing transaction + pool.ReturnInternalConnection(conn!, owner); + + // Now complete the transaction + scope.Complete(); + Interlocked.Increment(ref successCount); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.Equal(threadCount * iterationsPerThread, successCount); + AssertPoolMetrics(pool, 20, "After returning before transaction complete"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_CompleteBeforeReturn_ManyOperations() + { + // Arrange - Test completing transaction before returning connection + var pool = CreatePool(maxPoolSize: 20); + const int threadCount = 15; + const int iterationsPerThread = 100; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + var successCount = 0; + + try + { + // Act - Complete transaction before returning connection + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + DbConnectionInternal? conn = null; + SqlConnection? owner = null; + + using (var scope = new TransactionScope()) + { + owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); + Assert.NotNull(conn); + + // Complete transaction BEFORE returning + scope.Complete(); + } // Transaction completes here + + // Return connection AFTER transaction scope disposal + pool.ReturnInternalConnection(conn!, owner!); + Interlocked.Increment(ref successCount); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.Equal(threadCount * iterationsPerThread, successCount); + AssertPoolMetrics(pool, 20, "After completing before return"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_MixedCompletionOrder_RandomizedConcurrent() + { + // Arrange - Randomly mix the order of completion and return + var pool = CreatePool(maxPoolSize: 25); + const int totalOperations = 1000; + var exceptions = new ConcurrentBag(); + var orderCounts = new ConcurrentDictionary(); + + try + { + // Act - Randomize order across many operations + Parallel.For(0, totalOperations, new ParallelOptions { MaxDegreeOfParallelism = 30 }, i => + { + try + { + // Randomly choose order + bool returnBeforeComplete = i % 2 == 0; + + if (returnBeforeComplete) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=30", null), out var conn); + Assert.NotNull(conn); + + pool.ReturnInternalConnection(conn!, owner); + scope.Complete(); + orderCounts.AddOrUpdate("ReturnFirst", 1, (k, v) => v + 1); + } + else + { + DbConnectionInternal? conn = null; + SqlConnection? owner = null; + + using (var scope = new TransactionScope()) + { + owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=30", null), out conn); + Assert.NotNull(conn); + scope.Complete(); + } + + pool.ReturnInternalConnection(conn!, owner!); + orderCounts.AddOrUpdate("CompleteFirst", 1, (k, v) => v + 1); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + + // Assert + Assert.Empty(exceptions); + Assert.Equal(2, orderCounts.Count); // Should have both order types + Assert.True(orderCounts["ReturnFirst"] > 0); + Assert.True(orderCounts["CompleteFirst"] > 0); + AssertPoolMetrics(pool, 25, "After mixed completion order"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_InterleavedCompletionOrder_HighConcurrency() + { + // Arrange - Multiple threads with different patterns + var pool = CreatePool(maxPoolSize: 30); + const int threadCount = 20; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act - Each thread uses different pattern + for (int t = 0; t < threadCount; t++) + { + int threadIndex = t; + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + // Use thread index to determine pattern + switch (threadIndex % 4) + { + case 0: // Return before complete + using (var scope = new TransactionScope()) + { + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + pool.ReturnInternalConnection(conn!, owner); + scope.Complete(); + } + break; + + case 1: // Complete before return + { + DbConnectionInternal? conn; + SqlConnection owner; + using (var scope = new TransactionScope()) + { + owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); + scope.Complete(); + } + pool.ReturnInternalConnection(conn!, owner); + } + break; + + case 2: // Rollback before return + { + DbConnectionInternal? conn; + SqlConnection owner; + using (var scope = new TransactionScope()) + { + owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); + // Don't complete - rollback + } + pool.ReturnInternalConnection(conn!, owner); + } + break; + + case 3: // Return before rollback + using (var scope = new TransactionScope()) + { + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + pool.ReturnInternalConnection(conn!, owner); + // Don't complete - rollback + } + break; + } + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 30, "After interleaved completion order patterns"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_DelayedReturn_AfterTransactionDisposal() + { + // Arrange - Test returning connections with varying delays after transaction disposal + var pool = CreatePool(maxPoolSize: 20); + const int threadCount = 15; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + DbConnectionInternal? conn = null; + SqlConnection? owner = null; + + using (var scope = new TransactionScope()) + { + owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); + Assert.NotNull(conn); + scope.Complete(); + } // Transaction disposed here + + // Delay before returning (simulates held connection) + Thread.Sleep(s_random.Value!.Next(1, 10)); + + pool.ReturnInternalConnection(conn!, owner!); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 20, "After delayed return post-disposal"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_MultipleConnectionsSameTransaction_VariedReturnOrder() + { + // Arrange - Test multiple connections in same transaction returned in different orders + var pool = CreatePool(maxPoolSize: 30); + const int threadCount = 10; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + int threadIndex = t; + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner1 = new SqlConnection(); + var owner2 = new SqlConnection(); + var owner3 = new SqlConnection(); + + // Get multiple connections + pool.TryGetConnection(owner1, null, new DbConnectionOptions("", null), out var conn1); + pool.TryGetConnection(owner2, null, new DbConnectionOptions("", null), out var conn2); + pool.TryGetConnection(owner3, null, new DbConnectionOptions("", null), out var conn3); + + Assert.NotNull(conn1); + Assert.NotNull(conn2); + Assert.NotNull(conn3); + + // Return in different orders based on iteration + switch (i % 3) + { + case 0: // Return in order + pool.ReturnInternalConnection(conn1!, owner1); + pool.ReturnInternalConnection(conn2!, owner2); + pool.ReturnInternalConnection(conn3!, owner3); + break; + + case 1: // Return in reverse order + pool.ReturnInternalConnection(conn3!, owner3); + pool.ReturnInternalConnection(conn2!, owner2); + pool.ReturnInternalConnection(conn1!, owner1); + break; + + case 2: // Return in mixed order + pool.ReturnInternalConnection(conn2!, owner2); + pool.ReturnInternalConnection(conn1!, owner1); + pool.ReturnInternalConnection(conn3!, owner3); + break; + } + + scope.Complete(); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 30, "After varied return order for multiple connections"); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public async Task StressTest_AsyncCompletionOrder_MixedPatterns() + { + // Arrange - Test async scenarios with different completion orders + var pool = CreatePool(maxPoolSize: 25); + const int taskCount = 20; + const int iterationsPerTask = 30; + var exceptions = new ConcurrentBag(); + + try + { + // Act + var tasks = Enumerable.Range(0, taskCount).Select(async taskIndex => + { + try + { + for (int i = 0; i < iterationsPerTask; i++) + { + if (taskIndex % 2 == 0) + { + // Pattern 1: Return before async complete + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); + var tcs = new TaskCompletionSource(); + + pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); + if (conn == null) + { + conn = await tcs.Task; + } + + pool.ReturnInternalConnection(conn!, owner); + await Task.Delay(s_random.Value!.Next(1, 5)); + scope.Complete(); + } + else + { + // Pattern 2: Complete before async return + DbConnectionInternal? conn; + SqlConnection owner; using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + owner = new SqlConnection(); + var tcs = new TaskCompletionSource(); + + pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out conn); + if (conn == null) + { + conn = await tcs.Task; + } + + await Task.Delay(s_random.Value!.Next(1, 5)); + scope.Complete(); + } + + pool.ReturnInternalConnection(conn!, owner); + } + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }).ToArray(); await Task.WhenAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool, 25, "After async mixed completion patterns"); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + #region Mock Classes + + internal class MockSqlConnectionFactory : SqlConnectionFactory + { + protected override DbConnectionInternal CreateConnection( + DbConnectionOptions options, + DbConnectionPoolKey poolKey, + DbConnectionPoolGroupProviderInfo poolGroupProviderInfo, + IDbConnectionPool pool, + DbConnection owningConnection, + DbConnectionOptions userOptions) + { + return new MockDbConnectionInternal(); + } + } + + internal class MockDbConnectionInternal : DbConnectionInternal + { + private static int s_nextId = 1; + public int MockId { get; } = Interlocked.Increment(ref s_nextId); + + public override string ServerVersion => "Mock"; + + public override DbTransaction BeginTransaction(System.Data.IsolationLevel il) + { + throw new NotImplementedException(); + } + + public override void EnlistTransaction(Transaction? transaction) + { + // Mock implementation - handle transaction enlistment + if (transaction != null) + { + // Simulate successful enlistment + } + } + + protected override void Activate(Transaction? transaction) + { + // Mock implementation - activate connection + } + + protected override void Deactivate() + { + // Mock implementation - deactivate connection + } + + public override string ToString() => $"MockConnection_{MockId}"; + } + + #endregion +} From 3c74365dccb1afef66afd5cdf76d6af11a7a51ff Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Thu, 13 Nov 2025 15:13:09 -0800 Subject: [PATCH 02/18] Expose transacted connection in debug to assert transacted pool state. --- .../ConnectionPool/TransactedConnectionPool.cs | 6 +++++- .../ConnectionPool/WaitHandleDbConnectionPool.cs | 4 ++++ ...tHandleDbConnectionPoolTransactionStressTest.cs | 14 +++++++------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs index b8e56bd9e3..86f2f580b2 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs @@ -30,7 +30,7 @@ internal class TransactedConnectionPool /// A specialized list that holds database connections associated with a specific transaction. /// Maintains a reference to the transaction for proper cleanup when the transaction completes. /// - private sealed class TransactedConnectionList : List + internal sealed class TransactedConnectionList : List { private readonly Transaction _transaction; @@ -97,6 +97,10 @@ internal TransactedConnectionPool(IDbConnectionPool pool) /// The IDbConnectionPool instance that owns this transacted pool. internal IDbConnectionPool Pool { get; } + #if DEBUG + internal Dictionary TransactedConnections => _transactedCxns; + #endif + #endregion #region Methods diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs index c588fcd3f7..af555e0ac0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs @@ -328,6 +328,10 @@ public bool IsRunning private bool UsingIntegrateSecurity => _identity != null && DbConnectionPoolIdentity.NoIdentity != _identity; + #if DEBUG + internal TransactedConnectionPool TransactedConnectionPool => _transactedConnectionPool; + #endif + private void CleanupCallback(object state) { // Called when the cleanup-timer ticks over. diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index fb8ae57212..4d17c5ea39 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -6,19 +6,15 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Data.Common; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Transactions; -using Microsoft.Data.Common; using Microsoft.Data.Common.ConnectionString; using Microsoft.Data.ProviderBase; using Microsoft.Data.SqlClient.ConnectionPool; using Xunit; -#nullable enable - namespace Microsoft.Data.SqlClient.UnitTests.ConnectionPool; /// @@ -33,7 +29,7 @@ public class WaitHandleDbConnectionPoolTransactionStressTest private const int DefaultCreationTimeout = 15; // Thread-safe random number generator for .NET Framework compatibility - private static readonly ThreadLocal s_random = new ThreadLocal(() => new Random(Guid.NewGuid().GetHashCode())); + private static readonly ThreadLocal s_random = new(() => new Random(Guid.NewGuid().GetHashCode())); #region Helper Methods @@ -76,6 +72,12 @@ private void AssertPoolMetrics(WaitHandleDbConnectionPool pool, int expectedMaxC $"{context}: Pool count ({pool.Count}) exceeded max pool size ({expectedMaxCount})"); Assert.True(pool.Count >= 0, $"{context}: Pool count ({pool.Count}) is negative"); + + +#if DEBUG + var transactedConnections = pool.TransactedConnectionPool.TransactedConnections; + Assert.Empty(transactedConnections); +#endif } #endregion @@ -331,7 +333,6 @@ public void StressTest_MaxPoolSaturation_WithTransactions() try { // Synchronize all threads to start at once - barrier.SignalAndWait(); for (int i = 0; i < iterationsPerThread; i++) { @@ -347,7 +348,6 @@ public void StressTest_MaxPoolSaturation_WithTransactions() if (obtained && connection != null) { // Hold connection briefly - Thread.Sleep(s_random.Value!.Next(1, 10)); pool.ReturnInternalConnection(connection, owner); } From 39b785126f778d174f19d40070cad4b466a782b4 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Thu, 13 Nov 2025 15:45:24 -0800 Subject: [PATCH 03/18] Remove unnecessary context messages. --- ...leDbConnectionPoolTransactionStressTest.cs | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 4d17c5ea39..1f4eee591e 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -66,12 +66,12 @@ private WaitHandleDbConnectionPool CreatePool( return pool; } - private void AssertPoolMetrics(WaitHandleDbConnectionPool pool, int expectedMaxCount, string context) + private void AssertPoolMetrics(WaitHandleDbConnectionPool pool, int expectedMaxCount) { Assert.True(pool.Count <= expectedMaxCount, - $"{context}: Pool count ({pool.Count}) exceeded max pool size ({expectedMaxCount})"); + $"Pool count ({pool.Count}) exceeded max pool size ({expectedMaxCount})"); Assert.True(pool.Count >= 0, - $"{context}: Pool count ({pool.Count}) is negative"); + $"Pool count ({pool.Count}) is negative"); #if DEBUG @@ -115,7 +115,7 @@ public void StressTest_RapidTransactionOpenClose_SingleThreaded() } // Assert - AssertPoolMetrics(pool, 10, "After rapid single-threaded transactions"); + AssertPoolMetrics(pool, 10); } finally { @@ -174,7 +174,7 @@ public void StressTest_ConcurrentTransactions_MultipleThreads() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 20, "After concurrent transactions"); + AssertPoolMetrics(pool, 20); } finally { @@ -241,7 +241,7 @@ public void StressTest_InterminledTransactions_RapidScopeChanges() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30, "After intermingled transactions"); + AssertPoolMetrics(pool, 30); } finally { @@ -302,7 +302,7 @@ public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 40, "After mixed transacted/non-transacted operations"); + AssertPoolMetrics(pool, 40); } finally { @@ -364,7 +364,7 @@ public void StressTest_MaxPoolSaturation_WithTransactions() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 25, "After pool saturation with transactions"); + AssertPoolMetrics(pool, 25); } finally { @@ -415,7 +415,7 @@ public void StressTest_RapidOpenCloseUnderLoad_ThousandsOfOperations() // Assert Assert.Empty(exceptions); Assert.True(completedOperations > 0, "No operations completed successfully"); - AssertPoolMetrics(pool, 30, "After thousands of rapid operations"); + AssertPoolMetrics(pool, 30); } finally { @@ -487,7 +487,7 @@ public void StressTest_TransactionAffinity_ConnectionReuse() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 20, "After transaction affinity test"); + AssertPoolMetrics(pool, 20); // We should see some connection reuse Assert.True(connectionReuseCounts.Values.Sum() > 0, "Expected some connection reuse within transactions"); } @@ -548,7 +548,7 @@ public void StressTest_TransactionIsolation_DifferentTransactionsDifferentConnec // Assert Assert.Empty(exceptions); Assert.Equal(transactionCount, transactionConnections.Count); - AssertPoolMetrics(pool, 30, "After transaction isolation test"); + AssertPoolMetrics(pool, 30); } finally { @@ -608,7 +608,7 @@ public async Task StressTest_AsyncTransactions_HighConcurrency() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30, "After async transactions"); + AssertPoolMetrics(pool, 30); } finally { @@ -674,7 +674,7 @@ public void StressTest_TransactionRollback_ManyOperations() // Assert Assert.Empty(exceptions); Assert.True(rollbackCount > 0, "Expected some rollbacks"); - AssertPoolMetrics(pool, 20, "After transaction rollbacks"); + AssertPoolMetrics(pool, 20); } finally { @@ -732,7 +732,7 @@ public void StressTest_MixedTransactionOutcomes_Concurrent() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30, "After mixed transaction outcomes"); + AssertPoolMetrics(pool, 30); Assert.True(outcomes.Values.Sum() > 0, "Expected some transactions to complete"); } finally @@ -801,7 +801,7 @@ public void StressTest_RapidPoolShutdownDuringTransactions() } // Assert - Just verify no crash occurred and pool count is valid - AssertPoolMetrics(pool, 15, "After rapid shutdown during transactions"); + AssertPoolMetrics(pool, 15); } [Fact] @@ -857,7 +857,7 @@ public void StressTest_SingleConnectionPool_HighContention() // Assert Assert.Empty(exceptions); Assert.True(successCount > 0, "Expected some successful operations despite high contention"); - AssertPoolMetrics(pool, 1, "After high contention on single connection"); + AssertPoolMetrics(pool, 1); } finally { @@ -917,7 +917,7 @@ public void StressTest_ReturnBeforeTransactionComplete_ManyOperations() // Assert Assert.Empty(exceptions); Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool, 20, "After returning before transaction complete"); + AssertPoolMetrics(pool, 20); } finally { @@ -977,7 +977,7 @@ public void StressTest_CompleteBeforeReturn_ManyOperations() // Assert Assert.Empty(exceptions); Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool, 20, "After completing before return"); + AssertPoolMetrics(pool, 20); } finally { @@ -1043,7 +1043,7 @@ public void StressTest_MixedCompletionOrder_RandomizedConcurrent() Assert.Equal(2, orderCounts.Count); // Should have both order types Assert.True(orderCounts["ReturnFirst"] > 0); Assert.True(orderCounts["CompleteFirst"] > 0); - AssertPoolMetrics(pool, 25, "After mixed completion order"); + AssertPoolMetrics(pool, 25); } finally { @@ -1137,7 +1137,7 @@ public void StressTest_InterleavedCompletionOrder_HighConcurrency() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30, "After interleaved completion order patterns"); + AssertPoolMetrics(pool, 30); } finally { @@ -1194,7 +1194,7 @@ public void StressTest_DelayedReturn_AfterTransactionDisposal() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 20, "After delayed return post-disposal"); + AssertPoolMetrics(pool, 20); } finally { @@ -1274,7 +1274,7 @@ public void StressTest_MultipleConnectionsSameTransaction_VariedReturnOrder() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30, "After varied return order for multiple connections"); + AssertPoolMetrics(pool, 30); } finally { @@ -1348,7 +1348,7 @@ public async Task StressTest_AsyncCompletionOrder_MixedPatterns() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 25, "After async mixed completion patterns"); + AssertPoolMetrics(pool, 25); } finally { From 1f4bc059a4df121c81285af90f65d70b292610e0 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Fri, 14 Nov 2025 09:36:41 -0800 Subject: [PATCH 04/18] Test cleanup --- ...leDbConnectionPoolTransactionStressTest.cs | 232 +++++------------- 1 file changed, 55 insertions(+), 177 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 1f4eee591e..e9b4fd2998 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -84,84 +84,94 @@ private void AssertPoolMetrics(WaitHandleDbConnectionPool pool, int expectedMaxC #region Basic Transaction Stress Tests - [Fact] - public void StressTest_RapidTransactionOpenClose_SingleThreaded() + [Theory] + [InlineData(10, 100)] + public void StressTest_ConcurrentTransactions_MultipleThreads(int threadCount, int iterationsPerThread) { // Arrange - var pool = CreatePool(maxPoolSize: 10); - const int iterations = 1000; - var connections = new List(); + var pool = CreatePool(); + var tasks = new Task[threadCount]; try { // Act - for (int i = 0; i < iterations; i++) + for (int t = 0; t < threadCount; t++) { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); + tasks[t] = Task.Run(() => + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); - var obtained = pool.TryGetConnection( - owner, - taskCompletionSource: null, - new DbConnectionOptions("", null), - out DbConnectionInternal? connection); + var obtained = pool.TryGetConnection( + owner, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? connection); - Assert.True(obtained); - Assert.NotNull(connection); - connections.Add(connection); + Assert.True(obtained); + Assert.NotNull(connection); + + // Simulate some work + Thread.Sleep(1); - pool.ReturnInternalConnection(connection, owner); - scope.Complete(); + pool.ReturnInternalConnection(connection, owner); + scope.Complete(); + } + }); } + Task.WaitAll(tasks); + // Assert - AssertPoolMetrics(pool, 10); + AssertPoolMetrics(pool, 20); } finally { pool.Shutdown(); + pool.Clear(); } } [Fact] - public void StressTest_ConcurrentTransactions_MultipleThreads() + public void StressTest_TransactionIsolation_DifferentTransactionsDifferentConnections() { // Arrange - var pool = CreatePool(maxPoolSize: 20); - const int threadCount = 10; - const int iterationsPerThread = 100; - var tasks = new Task[threadCount]; + var pool = CreatePool(maxPoolSize: 30); + const int transactionCount = 100; + var tasks = new Task[transactionCount]; var exceptions = new ConcurrentBag(); + var transactionConnections = new ConcurrentDictionary>(); try { - // Act - for (int t = 0; t < threadCount; t++) + // Act - Each transaction should be isolated + for (int t = 0; t < transactionCount; t++) { tasks[t] = Task.Run(() => { try { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); - - var obtained = pool.TryGetConnection( - owner, - taskCompletionSource: null, - new DbConnectionOptions("", null), - out DbConnectionInternal? connection); - - Assert.True(obtained); - Assert.NotNull(connection); + using var scope = new TransactionScope(); + var transaction = Transaction.Current; + Assert.NotNull(transaction); + var txId = transaction!.TransactionInformation.LocalIdentifier; - // Simulate some work - Thread.Sleep(1); + var connectionIds = new List(); - pool.ReturnInternalConnection(connection, owner); - scope.Complete(); + // Get multiple connections within same transaction + for (int i = 0; i < 3; i++) + { + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + connectionIds.Add(conn!.ObjectID); + pool.ReturnInternalConnection(conn, owner); } + + transactionConnections[txId] = connectionIds; + scope.Complete(); } catch (Exception ex) { @@ -174,7 +184,8 @@ public void StressTest_ConcurrentTransactions_MultipleThreads() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 20); + Assert.Equal(transactionCount, transactionConnections.Count); + AssertPoolMetrics(pool, 30); } finally { @@ -425,139 +436,6 @@ public void StressTest_RapidOpenCloseUnderLoad_ThousandsOfOperations() #endregion - #region Transaction Affinity Stress Tests - - [Fact] - public void StressTest_TransactionAffinity_ConnectionReuse() - { - // Arrange - var pool = CreatePool(maxPoolSize: 20, hasTransactionAffinity: true); - const int threadCount = 10; - const int iterationsPerThread = 50; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - var connectionReuseCounts = new ConcurrentDictionary(); - - try - { - // Act - Test that connections are properly reused within same transaction - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(() => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(); - var owner1 = new SqlConnection(); - var owner2 = new SqlConnection(); - - // Get first connection - pool.TryGetConnection(owner1, null, new DbConnectionOptions("", null), out var conn1); - Assert.NotNull(conn1); - var conn1Id = conn1!.ObjectID; - - // Return it - pool.ReturnInternalConnection(conn1, owner1); - - // Get second connection in same transaction - pool.TryGetConnection(owner2, null, new DbConnectionOptions("", null), out var conn2); - Assert.NotNull(conn2); - var conn2Id = conn2!.ObjectID; - - // Track if we got the same connection back - if (conn1Id == conn2Id) - { - connectionReuseCounts.AddOrUpdate(Thread.CurrentThread.ManagedThreadId, 1, (k, v) => v + 1); - } - - pool.ReturnInternalConnection(conn2, owner2); - scope.Complete(); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool, 20); - // We should see some connection reuse - Assert.True(connectionReuseCounts.Values.Sum() > 0, "Expected some connection reuse within transactions"); - } - finally - { - pool.Shutdown(); - } - } - - [Fact] - public void StressTest_TransactionIsolation_DifferentTransactionsDifferentConnections() - { - // Arrange - var pool = CreatePool(maxPoolSize: 30); - const int transactionCount = 100; - var tasks = new Task[transactionCount]; - var exceptions = new ConcurrentBag(); - var transactionConnections = new ConcurrentDictionary>(); - - try - { - // Act - Each transaction should be isolated - for (int t = 0; t < transactionCount; t++) - { - tasks[t] = Task.Run(() => - { - try - { - using var scope = new TransactionScope(); - var transaction = Transaction.Current; - Assert.NotNull(transaction); - var txId = transaction!.TransactionInformation.LocalIdentifier; - - var connectionIds = new List(); - - // Get multiple connections within same transaction - for (int i = 0; i < 3; i++) - { - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - connectionIds.Add(conn!.ObjectID); - pool.ReturnInternalConnection(conn, owner); - } - - transactionConnections[txId] = connectionIds; - scope.Complete(); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - Assert.Equal(transactionCount, transactionConnections.Count); - AssertPoolMetrics(pool, 30); - } - finally - { - pool.Shutdown(); - } - } - - #endregion - #region Async Transaction Stress Tests [Fact] From fab9a43313fed72cc3ddc807d86a96f92f8885bb Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Thu, 20 Nov 2025 10:57:18 -0800 Subject: [PATCH 05/18] Convert TransactedConnections to auto-property. --- .../TransactedConnectionPool.cs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs index 86f2f580b2..6c94daa69f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs @@ -59,9 +59,6 @@ internal void Dispose() } #region Fields - - private readonly Dictionary _transactedCxns; - private static int _objectTypeCount; internal readonly int _objectID = System.Threading.Interlocked.Increment(ref _objectTypeCount); @@ -79,7 +76,7 @@ internal void Dispose() internal TransactedConnectionPool(IDbConnectionPool pool) { Pool = pool; - _transactedCxns = new Dictionary(); + TransactedConnections = new Dictionary(); SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Constructed for connection pool {1}", Id, Pool.Id); } @@ -97,9 +94,7 @@ internal TransactedConnectionPool(IDbConnectionPool pool) /// The IDbConnectionPool instance that owns this transacted pool. internal IDbConnectionPool Pool { get; } - #if DEBUG - internal Dictionary TransactedConnections => _transactedCxns; - #endif + internal Dictionary TransactedConnections { get; private set; } #endregion @@ -126,9 +121,9 @@ internal TransactedConnectionPool(IDbConnectionPool pool) TransactedConnectionList? connections; bool txnFound = false; - lock (_transactedCxns) + lock (TransactedConnections) { - txnFound = _transactedCxns.TryGetValue(transaction, out connections); + txnFound = TransactedConnections.TryGetValue(transaction, out connections); } // NOTE: GetTransactedObject is only used when AutoEnlist = True and the ambient transaction @@ -185,10 +180,10 @@ internal void PutTransactedObject(Transaction transaction, DbConnectionInternal // NOTE: because TransactionEnded is an asynchronous notification, there's no guarantee // around the order in which PutTransactionObject and TransactionEnded are called. - lock (_transactedCxns) + lock (TransactedConnections) { // Check if a transacted pool has been created for this transaction - if ((txnFound = _transactedCxns.TryGetValue(transaction, out connections)) + if ((txnFound = TransactedConnections.TryGetValue(transaction, out connections)) && connections is not null) { // synchronize multi-threaded access with GetTransactedObject @@ -216,14 +211,14 @@ internal void PutTransactedObject(Transaction transaction, DbConnectionInternal transactionClone = transaction.Clone(); newConnections = new TransactedConnectionList(2, transactionClone); // start with only two connections in the list; most times we won't need that many. - lock (_transactedCxns) + lock (TransactedConnections) { // NOTE: in the interim between the locks on the transacted pool (this) during // execution of this method, another thread (threadB) may have attempted to // add a different connection to the transacted pool under the same // transaction. As a result, threadB may have completed creating the // transacted pool while threadA was processing the above instructions. - if (_transactedCxns.TryGetValue(transaction, out connections) + if (TransactedConnections.TryGetValue(transaction, out connections) && connections is not null) { // synchronize multi-threaded access with GetTransactedObject @@ -241,7 +236,7 @@ internal void PutTransactedObject(Transaction transaction, DbConnectionInternal // add the connection/transacted object to the list newConnections.Add(transactedObject); - _transactedCxns.Add(transactionClone, newConnections); + TransactedConnections.Add(transactionClone, newConnections); transactionClone = null; // we've used it -- don't throw it or the TransactedConnectionList that references it away. } } @@ -300,9 +295,9 @@ internal void TransactionEnded(Transaction transaction, DbConnectionInternal tra // TODO: that the pending creation of a transacted pool for this transaction is aborted when // TODO: PutTransactedObject finally gets some CPU time? - lock (_transactedCxns) + lock (TransactedConnections) { - if (_transactedCxns.TryGetValue(transaction, out connections) + if (TransactedConnections.TryGetValue(transaction, out connections) && connections is not null) { bool shouldDisposeConnections = false; @@ -322,7 +317,7 @@ internal void TransactionEnded(Transaction transaction, DbConnectionInternal tra if (0 >= connections.Count) { SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Transaction {1}, Removing List from transacted pool.", Id, transaction.GetHashCode()); - _transactedCxns.Remove(transaction); + TransactedConnections.Remove(transaction); // we really need to dispose our connection list; it may have // native resources via the tx and GC may not happen soon enough. From cddc9ebdea132c2b02fa7b5abe6d786d92378c1b Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Thu, 20 Nov 2025 10:57:52 -0800 Subject: [PATCH 06/18] Make TransactedConnectionPool accessible when not in DEBUG. --- .../SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs | 2 -- .../WaitHandleDbConnectionPoolTransactionStressTest.cs | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs index af555e0ac0..ddc6b99720 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs @@ -328,9 +328,7 @@ public bool IsRunning private bool UsingIntegrateSecurity => _identity != null && DbConnectionPoolIdentity.NoIdentity != _identity; - #if DEBUG internal TransactedConnectionPool TransactedConnectionPool => _transactedConnectionPool; - #endif private void CleanupCallback(object state) { diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index e9b4fd2998..93ed4f9289 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -72,12 +72,7 @@ private void AssertPoolMetrics(WaitHandleDbConnectionPool pool, int expectedMaxC $"Pool count ({pool.Count}) exceeded max pool size ({expectedMaxCount})"); Assert.True(pool.Count >= 0, $"Pool count ({pool.Count}) is negative"); - - -#if DEBUG - var transactedConnections = pool.TransactedConnectionPool.TransactedConnections; - Assert.Empty(transactedConnections); -#endif + Assert.Empty(pool.TransactedConnectionPool.TransactedConnections); } #endregion From 9ba0b9c056bfc111718a4d968c08b25a35fe08ae Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Fri, 21 Nov 2025 08:58:41 -0800 Subject: [PATCH 07/18] Clean up and fix tests. Expose min and max pool size properties. --- .../WaitHandleDbConnectionPool.cs | 6 +- ...leDbConnectionPoolTransactionStressTest.cs | 319 +++++++++++++++--- 2 files changed, 269 insertions(+), 56 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs index ddc6b99720..701fc46c21 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs @@ -309,9 +309,9 @@ public bool IsRunning get { return State is Running; } } - private int MaxPoolSize => PoolGroupOptions.MaxPoolSize; + internal int MaxPoolSize => PoolGroupOptions.MaxPoolSize; - private int MinPoolSize => PoolGroupOptions.MinPoolSize; + internal int MinPoolSize => PoolGroupOptions.MinPoolSize; public DbConnectionPoolGroup PoolGroup => _connectionPoolGroup; @@ -948,6 +948,8 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj // If automatic transaction enlistment is required, then we try to // get the connection from the transacted connection pool first. + // If automatic enlistment is not enabled, then we cannot vend connections + // from the transacted pool. if (HasTransactionAffinity) { obj = GetFromTransactedPool(out transaction); diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 93ed4f9289..dcd987f79e 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -26,7 +26,7 @@ public class WaitHandleDbConnectionPoolTransactionStressTest { private const int DefaultMaxPoolSize = 50; private const int DefaultMinPoolSize = 0; - private const int DefaultCreationTimeout = 15; + private readonly int DefaultCreationTimeout = TimeSpan.FromSeconds(15).Milliseconds; // Thread-safe random number generator for .NET Framework compatibility private static readonly ThreadLocal s_random = new(() => new Random(Guid.NewGuid().GetHashCode())); @@ -66,10 +66,10 @@ private WaitHandleDbConnectionPool CreatePool( return pool; } - private void AssertPoolMetrics(WaitHandleDbConnectionPool pool, int expectedMaxCount) + private void AssertPoolMetrics(WaitHandleDbConnectionPool pool) { - Assert.True(pool.Count <= expectedMaxCount, - $"Pool count ({pool.Count}) exceeded max pool size ({expectedMaxCount})"); + Assert.True(pool.Count <= pool.MaxPoolSize, + $"Pool count ({pool.Count}) exceeded max pool size ({pool.MaxPoolSize})"); Assert.True(pool.Count >= 0, $"Pool count ({pool.Count}) is negative"); Assert.Empty(pool.TransactedConnectionPool.TransactedConnections); @@ -81,8 +81,10 @@ private void AssertPoolMetrics(WaitHandleDbConnectionPool pool, int expectedMaxC [Theory] [InlineData(10, 100)] - public void StressTest_ConcurrentTransactions_MultipleThreads(int threadCount, int iterationsPerThread) + public void StressTest_TransactionPerIteration(int threadCount, int iterationsPerThread) { + // Tests many threads and iterations, with each iteration creating a transaction scope. + // Arrange var pool = CreatePool(); var tasks = new Task[threadCount]; @@ -120,7 +122,7 @@ public void StressTest_ConcurrentTransactions_MultipleThreads(int threadCount, i Task.WaitAll(tasks); // Assert - AssertPoolMetrics(pool, 20); + AssertPoolMetrics(pool); } finally { @@ -129,58 +131,211 @@ public void StressTest_ConcurrentTransactions_MultipleThreads(int threadCount, i } } - [Fact] - public void StressTest_TransactionIsolation_DifferentTransactionsDifferentConnections() + [Theory] + [InlineData(10, 100)] + public async Task StressTest_TransactionPerIteration_Async(int threadCount, int iterationsPerThread) { + // Tests many threads and iterations, with each iteration creating a transaction scope. + // Arrange - var pool = CreatePool(maxPoolSize: 30); - const int transactionCount = 100; - var tasks = new Task[transactionCount]; - var exceptions = new ConcurrentBag(); - var transactionConnections = new ConcurrentDictionary>(); + var pool = CreatePool(); + var tasks = new Task[threadCount]; + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(async () => + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); + + var tcs = new TaskCompletionSource(); + var obtained = pool.TryGetConnection( + owner, + taskCompletionSource: tcs, + new DbConnectionOptions("", null), + out DbConnectionInternal? connection); + + // Wait for the task if not obtained immediately + connection ??= await tcs.Task; + + Assert.NotNull(connection); + + // Simulate some work + Thread.Sleep(1); + + pool.ReturnInternalConnection(connection, owner); + scope.Complete(); + } + }); + } + + await Task.WhenAll(tasks); + + // Assert + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + pool.Clear(); + } + } + + [Theory] + [InlineData(10, 100)] + public void StressTest_TransactionPerThread(int threadCount, int iterationsPerThread) + { + // Arrange + var pool = CreatePool(); + var tasks = new Task[threadCount]; try { // Act - Each transaction should be isolated - for (int t = 0; t < transactionCount; t++) + for (int t = 0; t < threadCount; t++) { tasks[t] = Task.Run(() => { - try + using var scope = new TransactionScope(); + var transaction = Transaction.Current; + Assert.NotNull(transaction); + + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) { - using var scope = new TransactionScope(); - var transaction = Transaction.Current; - Assert.NotNull(transaction); - var txId = transaction!.TransactionInformation.LocalIdentifier; + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + pool.ReturnInternalConnection(conn, owner); + } - var connectionIds = new List(); + Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); - // Get multiple connections within same transaction - for (int i = 0; i < 3; i++) - { - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - connectionIds.Add(conn!.ObjectID); - pool.ReturnInternalConnection(conn, owner); - } + scope.Complete(); + }); + } - transactionConnections[txId] = connectionIds; - scope.Complete(); + Task.WaitAll(tasks); + + // Assert + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + [Theory] + [InlineData(10, 100)] + public async Task StressTest_TransactionPerThread_Async(int threadCount, int iterationsPerThread) + { + // Arrange + var pool = CreatePool(); + var tasks = new Task[threadCount]; + + try + { + // Act - Each transaction should be isolated + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(async () => + { + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var transaction = Transaction.Current; + Assert.NotNull(transaction); + + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) + { + var owner = new SqlConnection(); + // The transaction *must* be set as the AsyncState of the TaskCompletionSource. + // The + var tcs = new TaskCompletionSource(transaction); + pool.TryGetConnection( + owner, + tcs, + new DbConnectionOptions("", null), + out var conn); + + conn ??= await tcs.Task; + + Assert.NotNull(conn); + + pool.ReturnInternalConnection(conn, owner); + + Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); } - catch (Exception ex) + + Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); + + scope.Complete(); + }); + } + + await Task.WhenAll(tasks); + + // Assert + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + [Theory] + [InlineData(1, 100)] + public void StressTest_SingleSharedTransaction(int threadCount, int iterationsPerThread) + { + // Arrange + var pool = CreatePool(); + var tasks = new Task[threadCount]; + using var scope = new TransactionScope(); + var transaction = Transaction.Current; + Assert.NotNull(transaction); + + try + { + // Act - Each transaction should be isolated + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + + + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) { - exceptions.Add(ex); + using var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); } + + }); } Task.WaitAll(tasks); + Console.WriteLine($"{pool.TransactedConnectionPool.TransactedConnections[transaction].Count} transacted connections"); + scope.Complete(); + + while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction) + && pool.TransactedConnectionPool.TransactedConnections[transaction].Count > 0) + { + // Wait for transaction to be cleaned up + Console.WriteLine("Waiting for transaction cleanup..."); + Thread.Sleep(100); + } + // Assert - Assert.Empty(exceptions); - Assert.Equal(transactionCount, transactionConnections.Count); - AssertPoolMetrics(pool, 30); + AssertPoolMetrics(pool); } finally { @@ -188,6 +343,62 @@ public void StressTest_TransactionIsolation_DifferentTransactionsDifferentConnec } } + [Theory] + [InlineData(10, 100)] + public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int iterationsPerThread) + { + // Arrange + var pool = CreatePool(); + var tasks = new Task[threadCount]; + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var transaction = Transaction.Current; + Assert.NotNull(transaction); + + try + { + // Act - Each transaction should be isolated + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(async () => + { + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) + { + var owner = new SqlConnection(); + // The transaction *must* be set as the AsyncState of the TaskCompletionSource. + // The + var tcs = new TaskCompletionSource(transaction); + pool.TryGetConnection( + owner, + tcs, + new DbConnectionOptions("", null), + out var conn); + + conn ??= await tcs.Task; + + Assert.NotNull(conn); + + pool.ReturnInternalConnection(conn, owner); + + Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); + } + + Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); + }); + } + + await Task.WhenAll(tasks); + + scope.Complete(); + + // Assert + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } #endregion #region Intermingled Transaction Stress Tests @@ -247,7 +458,7 @@ public void StressTest_InterminledTransactions_RapidScopeChanges() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30); + AssertPoolMetrics(pool); } finally { @@ -308,7 +519,7 @@ public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 40); + AssertPoolMetrics(pool); } finally { @@ -370,7 +581,7 @@ public void StressTest_MaxPoolSaturation_WithTransactions() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 25); + AssertPoolMetrics(pool); } finally { @@ -421,7 +632,7 @@ public void StressTest_RapidOpenCloseUnderLoad_ThousandsOfOperations() // Assert Assert.Empty(exceptions); Assert.True(completedOperations > 0, "No operations completed successfully"); - AssertPoolMetrics(pool, 30); + AssertPoolMetrics(pool); } finally { @@ -481,7 +692,7 @@ public async Task StressTest_AsyncTransactions_HighConcurrency() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30); + AssertPoolMetrics(pool); } finally { @@ -547,7 +758,7 @@ public void StressTest_TransactionRollback_ManyOperations() // Assert Assert.Empty(exceptions); Assert.True(rollbackCount > 0, "Expected some rollbacks"); - AssertPoolMetrics(pool, 20); + AssertPoolMetrics(pool); } finally { @@ -605,7 +816,7 @@ public void StressTest_MixedTransactionOutcomes_Concurrent() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30); + AssertPoolMetrics(pool); Assert.True(outcomes.Values.Sum() > 0, "Expected some transactions to complete"); } finally @@ -674,7 +885,7 @@ public void StressTest_RapidPoolShutdownDuringTransactions() } // Assert - Just verify no crash occurred and pool count is valid - AssertPoolMetrics(pool, 15); + AssertPoolMetrics(pool); } [Fact] @@ -730,7 +941,7 @@ public void StressTest_SingleConnectionPool_HighContention() // Assert Assert.Empty(exceptions); Assert.True(successCount > 0, "Expected some successful operations despite high contention"); - AssertPoolMetrics(pool, 1); + AssertPoolMetrics(pool); } finally { @@ -790,7 +1001,7 @@ public void StressTest_ReturnBeforeTransactionComplete_ManyOperations() // Assert Assert.Empty(exceptions); Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool, 20); + AssertPoolMetrics(pool); } finally { @@ -850,7 +1061,7 @@ public void StressTest_CompleteBeforeReturn_ManyOperations() // Assert Assert.Empty(exceptions); Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool, 20); + AssertPoolMetrics(pool); } finally { @@ -916,7 +1127,7 @@ public void StressTest_MixedCompletionOrder_RandomizedConcurrent() Assert.Equal(2, orderCounts.Count); // Should have both order types Assert.True(orderCounts["ReturnFirst"] > 0); Assert.True(orderCounts["CompleteFirst"] > 0); - AssertPoolMetrics(pool, 25); + AssertPoolMetrics(pool); } finally { @@ -1010,7 +1221,7 @@ public void StressTest_InterleavedCompletionOrder_HighConcurrency() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30); + AssertPoolMetrics(pool); } finally { @@ -1067,7 +1278,7 @@ public void StressTest_DelayedReturn_AfterTransactionDisposal() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 20); + AssertPoolMetrics(pool); } finally { @@ -1147,7 +1358,7 @@ public void StressTest_MultipleConnectionsSameTransaction_VariedReturnOrder() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 30); + AssertPoolMetrics(pool); } finally { @@ -1221,7 +1432,7 @@ public async Task StressTest_AsyncCompletionOrder_MixedPatterns() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool, 25); + AssertPoolMetrics(pool); } finally { @@ -1264,13 +1475,13 @@ public override void EnlistTransaction(Transaction? transaction) // Mock implementation - handle transaction enlistment if (transaction != null) { - // Simulate successful enlistment + EnlistedTransaction = transaction; } } protected override void Activate(Transaction? transaction) { - // Mock implementation - activate connection + EnlistedTransaction = transaction; } protected override void Deactivate() From d4533c63e50bc364ae5712682691bd29593627b2 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Fri, 21 Nov 2025 09:51:17 -0800 Subject: [PATCH 08/18] Clean up dependencies. --- ...a.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj | 1 - .../netcore/ref/Microsoft.Data.SqlClient.csproj | 3 --- .../netcore/src/Microsoft.Data.SqlClient.csproj | 3 --- .../Microsoft.Data.SqlClient.ExtUtilities.csproj | 6 ------ 4 files changed, 13 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj index f1d7c76751..8bdb0e520a 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj @@ -36,7 +36,6 @@ - diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj index a1b8110fb8..bfc2745e90 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj @@ -42,9 +42,6 @@ - - - diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 950d7f83b1..3977c4e566 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -1102,9 +1102,6 @@ - - - diff --git a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.ExtUtilities/Microsoft.Data.SqlClient.ExtUtilities.csproj b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.ExtUtilities/Microsoft.Data.SqlClient.ExtUtilities.csproj index cd3ffeb61f..e4ce03ba47 100644 --- a/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.ExtUtilities/Microsoft.Data.SqlClient.ExtUtilities.csproj +++ b/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.ExtUtilities/Microsoft.Data.SqlClient.ExtUtilities.csproj @@ -6,12 +6,6 @@ - - - From 6cd9e82997abe9865e94cf3fb5fa1ca4ee98114a Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Fri, 21 Nov 2025 09:51:34 -0800 Subject: [PATCH 09/18] Add missing method override. --- .../WaitHandleDbConnectionPoolTransactionStressTest.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index dcd987f79e..0b000096fe 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -1490,6 +1490,11 @@ protected override void Deactivate() } public override string ToString() => $"MockConnection_{MockId}"; + + internal override void ResetConnection() + { + // Do nothing + } } #endregion From d4b1e2107f2188422da7a907c0743b30a75f8c64 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Fri, 21 Nov 2025 11:13:16 -0800 Subject: [PATCH 10/18] Fix shared transaction test case. --- ...leDbConnectionPoolTransactionStressTest.cs | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 0b000096fe..538f4cc926 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -290,44 +290,54 @@ public async Task StressTest_TransactionPerThread_Async(int threadCount, int ite } [Theory] - [InlineData(1, 100)] + [InlineData(10, 100)] public void StressTest_SingleSharedTransaction(int threadCount, int iterationsPerThread) { // Arrange var pool = CreatePool(); var tasks = new Task[threadCount]; - using var scope = new TransactionScope(); - var transaction = Transaction.Current; - Assert.NotNull(transaction); try { - // Act - Each transaction should be isolated - for (int t = 0; t < threadCount; t++) + Transaction? transaction = null; + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { - tasks[t] = Task.Run(() => + transaction = Transaction.Current; + Assert.NotNull(transaction); + // Act - Each transaction should be isolated + for (int t = 0; t < threadCount; t++) { - - - // Get multiple connections within same transaction - for (int i = 0; i < iterationsPerThread; i++) + tasks[t] = Task.Run(() => { - using var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - } + using (var innerScope = new TransactionScope(transaction)) + { + Assert.Equal(transaction, Transaction.Current); + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) + { + using var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); - - }); - } + // We bypass the SqlConnection.Open flow, so SqlConnection.InnerConnection is never set + // Therefore, SqlConnection.Close doesn't return the connection to the pool, we have to + // do it manually. + pool.ReturnInternalConnection(conn, owner); + } - Task.WaitAll(tasks); + innerScope.Complete(); + } + }); + } - Console.WriteLine($"{pool.TransactedConnectionPool.TransactedConnections[transaction].Count} transacted connections"); - scope.Complete(); + Task.WaitAll(tasks); + + //Console.WriteLine($"{pool.TransactedConnectionPool.TransactedConnections[transaction].Count} transacted connections"); + scope.Complete(); + } - while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction) - && pool.TransactedConnectionPool.TransactedConnections[transaction].Count > 0) + while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction!) + && pool.TransactedConnectionPool.TransactedConnections[transaction!].Count > 0) { // Wait for transaction to be cleaned up Console.WriteLine("Waiting for transaction cleanup..."); From e68ea14fcb0712014e293f192eba3b3c6e3e33b0 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Fri, 21 Nov 2025 11:44:22 -0800 Subject: [PATCH 11/18] Fix async single transaction test. --- ...leDbConnectionPoolTransactionStressTest.cs | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 538f4cc926..46fda16e1f 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -360,46 +360,61 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int // Arrange var pool = CreatePool(); var tasks = new Task[threadCount]; - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var transaction = Transaction.Current; - Assert.NotNull(transaction); try { - // Act - Each transaction should be isolated - for (int t = 0; t < threadCount; t++) + Transaction? transaction = null; + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { - tasks[t] = Task.Run(async () => + transaction = Transaction.Current; + Assert.NotNull(transaction); + // Act - Each transaction should be isolated + for (int t = 0; t < threadCount; t++) { - // Get multiple connections within same transaction - for (int i = 0; i < iterationsPerThread; i++) + tasks[t] = Task.Run(async () => { - var owner = new SqlConnection(); - // The transaction *must* be set as the AsyncState of the TaskCompletionSource. - // The - var tcs = new TaskCompletionSource(transaction); - pool.TryGetConnection( - owner, - tcs, - new DbConnectionOptions("", null), - out var conn); + using (var innerScope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) + { + Assert.Equal(transaction, Transaction.Current); + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) + { + var owner = new SqlConnection(); + // The transaction *must* be set as the AsyncState of the TaskCompletionSource. + // The + var tcs = new TaskCompletionSource(transaction); + pool.TryGetConnection( + owner, + tcs, + new DbConnectionOptions("", null), + out var conn); - conn ??= await tcs.Task; + conn ??= await tcs.Task; - Assert.NotNull(conn); + Assert.NotNull(conn); - pool.ReturnInternalConnection(conn, owner); + // Simulate some work + await Task.Delay(1); - Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); - } + pool.ReturnInternalConnection(conn, owner); + } - Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); - }); - } + innerScope.Complete(); + } + }); + } - await Task.WhenAll(tasks); + await Task.WhenAll(tasks); + scope.Complete(); + } - scope.Complete(); + while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction!) + && pool.TransactedConnectionPool.TransactedConnections[transaction!].Count > 0) + { + // Wait for transaction to be cleaned up + Console.WriteLine("Waiting for transaction cleanup..."); + await Task.Delay(100); + } // Assert AssertPoolMetrics(pool); From 1191ca4bfae3d20a0f9e2202d090e0d346b58b7a Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Fri, 21 Nov 2025 12:03:22 -0800 Subject: [PATCH 12/18] Remove uninteresting test cases. Add todos for missing test cases. --- ...leDbConnectionPoolTransactionStressTest.cs | 687 +----------------- 1 file changed, 8 insertions(+), 679 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 46fda16e1f..68ac75e4b6 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -404,7 +404,7 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int }); } - await Task.WhenAll(tasks); + //await Task.WhenAll(tasks); scope.Complete(); } @@ -426,70 +426,12 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int } #endregion - #region Intermingled Transaction Stress Tests - - [Fact] - public void StressTest_InterminledTransactions_RapidScopeChanges() - { - // Arrange - var pool = CreatePool(maxPoolSize: 30); - const int threadCount = 15; - const int iterationsPerThread = 50; - var tasks = new Task[threadCount]; - var totalConnections = new ConcurrentBag(); - var exceptions = new ConcurrentBag(); - - try - { - // Act - Each thread creates nested and sequential transactions - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(() => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - // Sequential transactions - using (var scope1 = new TransactionScope()) - { - var owner1 = new SqlConnection(); - pool.TryGetConnection(owner1, null, new DbConnectionOptions("", null), out var conn1); - Assert.NotNull(conn1); - totalConnections.Add(conn1); - pool.ReturnInternalConnection(conn1, owner1); - scope1.Complete(); - } - - using (var scope2 = new TransactionScope()) - { - var owner2 = new SqlConnection(); - pool.TryGetConnection(owner2, null, new DbConnectionOptions("", null), out var conn2); - Assert.NotNull(conn2); - totalConnections.Add(conn2); - pool.ReturnInternalConnection(conn2, owner2); - scope2.Complete(); - } - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); + // TODO saturate pool with open transactions and verify waits time out as expected + // TODO test with nested transactions + // TODO test with distributed transactions + // TODO find a way to test the race conditions where a connection is returned just as a transaction is completing, this should strand the connection in the transacted pool. - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } + #region Intermingled Transaction Stress Tests [Fact] public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() @@ -554,7 +496,6 @@ public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() #endregion - #region High Load Stress Tests [Fact] public void StressTest_MaxPoolSaturation_WithTransactions() @@ -614,120 +555,6 @@ public void StressTest_MaxPoolSaturation_WithTransactions() } } - [Fact] - public void StressTest_RapidOpenCloseUnderLoad_ThousandsOfOperations() - { - // Arrange - var pool = CreatePool(maxPoolSize: 30); - const int totalOperations = 5000; - const int maxParallelism = 40; - var exceptions = new ConcurrentBag(); - var completedOperations = 0; - - try - { - // Act - Perform thousands of rapid operations - Parallel.For(0, totalOperations, new ParallelOptions { MaxDegreeOfParallelism = maxParallelism }, i => - { - try - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); - - var obtained = pool.TryGetConnection( - owner, - null, - new DbConnectionOptions("Timeout=30", null), - out DbConnectionInternal? connection); - - if (obtained && connection != null) - { - pool.ReturnInternalConnection(connection, owner); - Interlocked.Increment(ref completedOperations); - } - - scope.Complete(); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - - // Assert - Assert.Empty(exceptions); - Assert.True(completedOperations > 0, "No operations completed successfully"); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - #endregion - - #region Async Transaction Stress Tests - - [Fact] - public async Task StressTest_AsyncTransactions_HighConcurrency() - { - // Arrange - var pool = CreatePool(maxPoolSize: 30); - const int taskCount = 30; // Reduced from 50 - const int iterationsPerTask = 10; // Reduced from 30 - var exceptions = new ConcurrentBag(); - - try - { - // Act - var tasks = Enumerable.Range(0, taskCount).Select(async t => - { - try - { - for (int i = 0; i < iterationsPerTask; i++) - { - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); - var tcs = new TaskCompletionSource(); - - var obtained = pool.TryGetConnection(owner, tcs, new DbConnectionOptions("Timeout=30", null), out var connection); - - if (!obtained) - { - connection = await tcs.Task; - } - - Assert.NotNull(connection); - - // Simulate async work - reduced delay - await Task.Delay(s_random.Value!.Next(1, 3)); - - pool.ReturnInternalConnection(connection!, owner); - scope.Complete(); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }).ToArray(); - - await Task.WhenAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - #endregion - - #region Transaction Completion Stress Tests [Fact] public void StressTest_TransactionRollback_ManyOperations() @@ -791,67 +618,6 @@ public void StressTest_TransactionRollback_ManyOperations() } } - [Fact] - public void StressTest_MixedTransactionOutcomes_Concurrent() - { - // Arrange - var pool = CreatePool(maxPoolSize: 30); - const int totalOperations = 1000; - var exceptions = new ConcurrentBag(); - var outcomes = new ConcurrentDictionary(); - - try - { - // Act - Random transaction outcomes - Parallel.For(0, totalOperations, new ParallelOptions { MaxDegreeOfParallelism = 20 }, i => - { - try - { - var outcome = i % 3; // 0=commit, 1=rollback, 2=exception-then-rollback - - using var scope = new TransactionScope(); - var owner = new SqlConnection(); - - pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=30", null), out var conn); - Assert.NotNull(conn); - - if (outcome == 0) - { - scope.Complete(); - outcomes.AddOrUpdate("Committed", 1, (k, v) => v + 1); - } - else if (outcome == 1) - { - // Rollback (no Complete call) - outcomes.AddOrUpdate("Rolledback", 1, (k, v) => v + 1); - } - else - { - // Exception then rollback - outcomes.AddOrUpdate("Exception", 1, (k, v) => v + 1); - } - - pool.ReturnInternalConnection(conn!, owner); - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - Assert.True(outcomes.Values.Sum() > 0, "Expected some transactions to complete"); - } - finally - { - pool.Shutdown(); - } - } - - #endregion - #region Edge Case Stress Tests [Fact] @@ -900,140 +666,16 @@ public void StressTest_RapidPoolShutdownDuringTransactions() Thread.Sleep(100); pool.Shutdown(); - try - { - Task.WaitAll(tasks); - } - catch - { - // Expected - some tasks may fail during shutdown - } + Task.WaitAll(tasks); // Assert - Just verify no crash occurred and pool count is valid AssertPoolMetrics(pool); } - [Fact] - public void StressTest_SingleConnectionPool_HighContention() - { - // Arrange - Pool size of 1 creates maximum contention - var pool = CreatePool(maxPoolSize: 1); - const int threadCount = 20; - const int iterationsPerThread = 50; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - var successCount = 0; - - try - { - // Act - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(() => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); - - var obtained = pool.TryGetConnection( - owner, - null, - new DbConnectionOptions("Timeout=30", null), - out DbConnectionInternal? conn); - - if (obtained && conn != null) - { - Thread.Sleep(1); // Hold briefly - pool.ReturnInternalConnection(conn, owner); - Interlocked.Increment(ref successCount); - } - - scope.Complete(); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - Assert.True(successCount > 0, "Expected some successful operations despite high contention"); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - #endregion #region Transaction Completion Order Tests - [Fact] - public void StressTest_ReturnBeforeTransactionComplete_ManyOperations() - { - // Arrange - Test returning connection before transaction scope completes - var pool = CreatePool(maxPoolSize: 20); - const int threadCount = 15; - const int iterationsPerThread = 100; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - var successCount = 0; - - try - { - // Act - Return connection before calling scope.Complete() - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(() => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); - - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - - // Return connection BEFORE completing transaction - pool.ReturnInternalConnection(conn!, owner); - - // Now complete the transaction - scope.Complete(); - Interlocked.Increment(ref successCount); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - [Fact] public void StressTest_CompleteBeforeReturn_ManyOperations() { @@ -1070,6 +712,7 @@ public void StressTest_CompleteBeforeReturn_ManyOperations() } // Transaction completes here // Return connection AFTER transaction scope disposal + // TODO: questionable, make sure we're not double returning pool.ReturnInternalConnection(conn!, owner!); Interlocked.Increment(ref successCount); } @@ -1094,166 +737,6 @@ public void StressTest_CompleteBeforeReturn_ManyOperations() } } - [Fact] - public void StressTest_MixedCompletionOrder_RandomizedConcurrent() - { - // Arrange - Randomly mix the order of completion and return - var pool = CreatePool(maxPoolSize: 25); - const int totalOperations = 1000; - var exceptions = new ConcurrentBag(); - var orderCounts = new ConcurrentDictionary(); - - try - { - // Act - Randomize order across many operations - Parallel.For(0, totalOperations, new ParallelOptions { MaxDegreeOfParallelism = 30 }, i => - { - try - { - // Randomly choose order - bool returnBeforeComplete = i % 2 == 0; - - if (returnBeforeComplete) - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=30", null), out var conn); - Assert.NotNull(conn); - - pool.ReturnInternalConnection(conn!, owner); - scope.Complete(); - orderCounts.AddOrUpdate("ReturnFirst", 1, (k, v) => v + 1); - } - else - { - DbConnectionInternal? conn = null; - SqlConnection? owner = null; - - using (var scope = new TransactionScope()) - { - owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=30", null), out conn); - Assert.NotNull(conn); - scope.Complete(); - } - - pool.ReturnInternalConnection(conn!, owner!); - orderCounts.AddOrUpdate("CompleteFirst", 1, (k, v) => v + 1); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - - // Assert - Assert.Empty(exceptions); - Assert.Equal(2, orderCounts.Count); // Should have both order types - Assert.True(orderCounts["ReturnFirst"] > 0); - Assert.True(orderCounts["CompleteFirst"] > 0); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - [Fact] - public void StressTest_InterleavedCompletionOrder_HighConcurrency() - { - // Arrange - Multiple threads with different patterns - var pool = CreatePool(maxPoolSize: 30); - const int threadCount = 20; - const int iterationsPerThread = 50; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - - try - { - // Act - Each thread uses different pattern - for (int t = 0; t < threadCount; t++) - { - int threadIndex = t; - tasks[t] = Task.Run(() => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - // Use thread index to determine pattern - switch (threadIndex % 4) - { - case 0: // Return before complete - using (var scope = new TransactionScope()) - { - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - pool.ReturnInternalConnection(conn!, owner); - scope.Complete(); - } - break; - - case 1: // Complete before return - { - DbConnectionInternal? conn; - SqlConnection owner; - using (var scope = new TransactionScope()) - { - owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); - scope.Complete(); - } - pool.ReturnInternalConnection(conn!, owner); - } - break; - - case 2: // Rollback before return - { - DbConnectionInternal? conn; - SqlConnection owner; - using (var scope = new TransactionScope()) - { - owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); - // Don't complete - rollback - } - pool.ReturnInternalConnection(conn!, owner); - } - break; - - case 3: // Return before rollback - using (var scope = new TransactionScope()) - { - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - pool.ReturnInternalConnection(conn!, owner); - // Don't complete - rollback - } - break; - } - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - [Fact] public void StressTest_DelayedReturn_AfterTransactionDisposal() { @@ -1311,160 +794,6 @@ public void StressTest_DelayedReturn_AfterTransactionDisposal() } } - [Fact] - public void StressTest_MultipleConnectionsSameTransaction_VariedReturnOrder() - { - // Arrange - Test multiple connections in same transaction returned in different orders - var pool = CreatePool(maxPoolSize: 30); - const int threadCount = 10; - const int iterationsPerThread = 50; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - - try - { - // Act - for (int t = 0; t < threadCount; t++) - { - int threadIndex = t; - tasks[t] = Task.Run(() => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(); - var owner1 = new SqlConnection(); - var owner2 = new SqlConnection(); - var owner3 = new SqlConnection(); - - // Get multiple connections - pool.TryGetConnection(owner1, null, new DbConnectionOptions("", null), out var conn1); - pool.TryGetConnection(owner2, null, new DbConnectionOptions("", null), out var conn2); - pool.TryGetConnection(owner3, null, new DbConnectionOptions("", null), out var conn3); - - Assert.NotNull(conn1); - Assert.NotNull(conn2); - Assert.NotNull(conn3); - - // Return in different orders based on iteration - switch (i % 3) - { - case 0: // Return in order - pool.ReturnInternalConnection(conn1!, owner1); - pool.ReturnInternalConnection(conn2!, owner2); - pool.ReturnInternalConnection(conn3!, owner3); - break; - - case 1: // Return in reverse order - pool.ReturnInternalConnection(conn3!, owner3); - pool.ReturnInternalConnection(conn2!, owner2); - pool.ReturnInternalConnection(conn1!, owner1); - break; - - case 2: // Return in mixed order - pool.ReturnInternalConnection(conn2!, owner2); - pool.ReturnInternalConnection(conn1!, owner1); - pool.ReturnInternalConnection(conn3!, owner3); - break; - } - - scope.Complete(); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - [Fact] - public async Task StressTest_AsyncCompletionOrder_MixedPatterns() - { - // Arrange - Test async scenarios with different completion orders - var pool = CreatePool(maxPoolSize: 25); - const int taskCount = 20; - const int iterationsPerTask = 30; - var exceptions = new ConcurrentBag(); - - try - { - // Act - var tasks = Enumerable.Range(0, taskCount).Select(async taskIndex => - { - try - { - for (int i = 0; i < iterationsPerTask; i++) - { - if (taskIndex % 2 == 0) - { - // Pattern 1: Return before async complete - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); - var tcs = new TaskCompletionSource(); - - pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); - if (conn == null) - { - conn = await tcs.Task; - } - - pool.ReturnInternalConnection(conn!, owner); - await Task.Delay(s_random.Value!.Next(1, 5)); - scope.Complete(); - } - else - { - // Pattern 2: Complete before async return - DbConnectionInternal? conn; - SqlConnection owner; using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) - { - owner = new SqlConnection(); - var tcs = new TaskCompletionSource(); - - pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out conn); - if (conn == null) - { - conn = await tcs.Task; - } - - await Task.Delay(s_random.Value!.Next(1, 5)); - scope.Complete(); - } - - pool.ReturnInternalConnection(conn!, owner); - } - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }).ToArray(); await Task.WhenAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - #endregion #region Mock Classes From b9698321db08e6572f87220db965a54a7b32e360 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Fri, 21 Nov 2025 14:52:03 -0800 Subject: [PATCH 13/18] Add more test cases. Speed up tests. --- ...leDbConnectionPoolTransactionStressTest.cs | 1117 ++++++++++++++--- 1 file changed, 910 insertions(+), 207 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 68ac75e4b6..67e4d71d10 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -94,11 +94,11 @@ public void StressTest_TransactionPerIteration(int threadCount, int iterationsPe // Act for (int t = 0; t < threadCount; t++) { - tasks[t] = Task.Run(() => + tasks[t] = Task.Run(async () => { for (int i = 0; i < iterationsPerThread; i++) { - using var scope = new TransactionScope(); + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var owner = new SqlConnection(); var obtained = pool.TryGetConnection( @@ -110,9 +110,6 @@ public void StressTest_TransactionPerIteration(int threadCount, int iterationsPe Assert.True(obtained); Assert.NotNull(connection); - // Simulate some work - Thread.Sleep(1); - pool.ReturnInternalConnection(connection, owner); scope.Complete(); } @@ -165,9 +162,6 @@ public async Task StressTest_TransactionPerIteration_Async(int threadCount, int Assert.NotNull(connection); - // Simulate some work - Thread.Sleep(1); - pool.ReturnInternalConnection(connection, owner); scope.Complete(); } @@ -393,9 +387,6 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int Assert.NotNull(conn); - // Simulate some work - await Task.Delay(1); - pool.ReturnInternalConnection(conn, owner); } @@ -426,54 +417,44 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int } #endregion - // TODO saturate pool with open transactions and verify waits time out as expected - // TODO test with nested transactions - // TODO test with distributed transactions - // TODO find a way to test the race conditions where a connection is returned just as a transaction is completing, this should strand the connection in the transacted pool. - - #region Intermingled Transaction Stress Tests + #region Pool Saturation and Timeout Tests [Fact] - public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() + public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() { - // Arrange - var pool = CreatePool(maxPoolSize: 40); - const int threadCount = 20; - const int iterationsPerThread = 50; - var tasks = new Task[threadCount]; + // Arrange - Test that when pool is saturated with transactions, new requests behave correctly + var pool = CreatePool(maxPoolSize: 3); + const int saturatingThreadCount = 3; + const int waitingThreadCount = 5; + var saturatingTasks = new Task[saturatingThreadCount]; + var waitingTasks = new Task[waitingThreadCount]; var exceptions = new ConcurrentBag(); + var completedWithoutConnection = 0; + var barrier = new Barrier(saturatingThreadCount + 1); try { - // Act - Half the threads use transactions, half don't - for (int t = 0; t < threadCount; t++) + // Act - Saturate the pool with long-held connections in transactions + for (int t = 0; t < saturatingThreadCount; t++) { - bool useTransactions = t % 2 == 0; - tasks[t] = Task.Run(() => + saturatingTasks[t] = Task.Run(async () => { try { - for (int i = 0; i < iterationsPerThread; i++) - { - if (useTransactions) - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - Thread.Sleep(s_random.Value!.Next(1, 5)); - pool.ReturnInternalConnection(conn, owner); - scope.Complete(); - } - else - { - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - Thread.Sleep(s_random.Value!.Next(1, 5)); - pool.ReturnInternalConnection(conn, owner); - } - } + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); + + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + + // Signal that we've acquired a connection + barrier.SignalAndWait(); + + // Hold the connection briefly + await Task.Delay(200); + + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); } catch (Exception ex) { @@ -482,72 +463,49 @@ public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() }); } - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - #endregion - - - [Fact] - public void StressTest_MaxPoolSaturation_WithTransactions() - { - // Arrange - var pool = CreatePool(maxPoolSize: 25); - const int threadCount = 50; // More threads than pool size - const int iterationsPerThread = 20; - var activeTasks = new ConcurrentBag(); - var exceptions = new ConcurrentBag(); - var barrier = new Barrier(threadCount); + // Wait for all saturating threads to acquire connections + barrier.SignalAndWait(); - try - { - // Act - Saturate the pool with transactions - var tasks = Enumerable.Range(0, threadCount).Select(t => Task.Run(() => + // Now try to get more connections - pool is saturated + for (int t = 0; t < waitingThreadCount; t++) { - try + waitingTasks[t] = Task.Run(() => { - // Synchronize all threads to start at once - - for (int i = 0; i < iterationsPerThread; i++) + try { using var scope = new TransactionScope(); var owner = new SqlConnection(); + // Try to get connection with short timeout var obtained = pool.TryGetConnection( owner, null, - new DbConnectionOptions("Timeout=30", null), - out DbConnectionInternal? connection); + new DbConnectionOptions("Connection Timeout=1", null), + out var conn); - if (obtained && connection != null) + if (!obtained || conn == null) { - // Hold connection briefly - pool.ReturnInternalConnection(connection, owner); + Interlocked.Increment(ref completedWithoutConnection); + } + else + { + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); } - - scope.Complete(); } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - })).ToArray(); + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } - Task.WaitAll(tasks); + Task.WaitAll(saturatingTasks.Concat(waitingTasks).ToArray()); // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool); + Assert.True(completedWithoutConnection >= 0, + $"Completed without connection: {completedWithoutConnection}"); } finally { @@ -555,141 +513,151 @@ public void StressTest_MaxPoolSaturation_WithTransactions() } } - [Fact] - public void StressTest_TransactionRollback_ManyOperations() + public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_Async() { - // Arrange - var pool = CreatePool(maxPoolSize: 20); - const int threadCount = 10; - const int iterationsPerThread = 100; - var tasks = new Task[threadCount]; + // Arrange - Test that when pool is saturated with transactions, new requests behave correctly + var pool = CreatePool(maxPoolSize: 3); + const int saturatingThreadCount = 3; + const int waitingThreadCount = 5; + var saturatingTasks = new Task[saturatingThreadCount]; + var waitingTasks = new Task[waitingThreadCount]; var exceptions = new ConcurrentBag(); - var rollbackCount = 0; + var completedWithoutConnection = 0; + + // Async-friendly barrier replacement + var allSaturatingThreadsReady = new TaskCompletionSource(); + var readyCount = 0; try { - // Act - Alternate between commit and rollback - for (int t = 0; t < threadCount; t++) + // Act - Saturate the pool with long-held connections in transactions + for (int t = 0; t < saturatingThreadCount; t++) { - tasks[t] = Task.Run(() => + saturatingTasks[t] = Task.Run(async () => { try { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); + var tcs = new TaskCompletionSource(Transaction.Current); + pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); + conn ??= await tcs.Task; - // Randomly commit or rollback - if (i % 2 == 0) - { - scope.Complete(); - } - else - { - Interlocked.Increment(ref rollbackCount); - // Don't call Complete - let it rollback - } + Assert.NotNull(conn); - pool.ReturnInternalConnection(conn!, owner); + // Signal that we've acquired a connection + if (Interlocked.Increment(ref readyCount) == saturatingThreadCount) + { + allSaturatingThreadsReady.TrySetResult(true); } + + // Wait for all saturating threads to be ready + await allSaturatingThreadsReady.Task; + + // Hold the connection briefly + await Task.Delay(200); + + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); } catch (Exception ex) { exceptions.Add(ex); + // Ensure barrier is released even on exception + if (Interlocked.Increment(ref readyCount) == saturatingThreadCount) + { + allSaturatingThreadsReady.TrySetResult(true); + } } }); } - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - Assert.True(rollbackCount > 0, "Expected some rollbacks"); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - #region Edge Case Stress Tests - - [Fact] - public void StressTest_RapidPoolShutdownDuringTransactions() - { - // Arrange - var pool = CreatePool(maxPoolSize: 15); - const int threadCount = 20; - var barrier = new Barrier(threadCount); - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); + // Wait for all saturating threads to acquire connections + await allSaturatingThreadsReady.Task; - // Act - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(() => + // Now start waiting threads + for (int t = 0; t < waitingThreadCount; t++) { - try + waitingTasks[t] = Task.Run(async () => { - barrier.SignalAndWait(); - - for (int i = 0; i < 50; i++) + try { - using var scope = new TransactionScope(); + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var owner = new SqlConnection(); - var obtained = pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=5", null), out var conn); + var tcs = new TaskCompletionSource(Transaction.Current); + var obtained = pool.TryGetConnection( + owner, + tcs, + new DbConnectionOptions("Connection Timeout=1", null), + out var conn); - if (obtained && conn != null) + if (!obtained) + { + // Try to wait with timeout + var timeoutTask = Task.Delay(300); + var completedTask = await Task.WhenAny(tcs.Task, timeoutTask); + + if (completedTask == timeoutTask) + { + Interlocked.Increment(ref completedWithoutConnection); + } + else + { + conn = await tcs.Task; + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); + } + } + else if (conn != null) { pool.ReturnInternalConnection(conn, owner); + scope.Complete(); + } + else + { + Interlocked.Increment(ref completedWithoutConnection); } - - scope.Complete(); } - } - catch (Exception ex) - { - // Some exceptions expected during shutdown - exceptions.Add(ex); - } - }); - } - - // Shutdown pool while operations are in progress - Thread.Sleep(100); - pool.Shutdown(); + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } - Task.WaitAll(tasks); + await Task.WhenAll(saturatingTasks.Concat(waitingTasks).ToArray()); - // Assert - Just verify no crash occurred and pool count is valid - AssertPoolMetrics(pool); + // Assert + Assert.Empty(exceptions); + Assert.True(completedWithoutConnection >= 0, + $"Completed without connection: {completedWithoutConnection}"); + } + finally + { + pool.Shutdown(); + } } #endregion - #region Transaction Completion Order Tests + #region Nested Transaction Tests - [Fact] - public void StressTest_CompleteBeforeReturn_ManyOperations() + [Theory] + [InlineData(5, 3, 10)] + public void StressTest_NestedTransactions_MultipleLevels(int threadCount, int nestingLevel, int iterationsPerThread) { - // Arrange - Test completing transaction before returning connection + // Arrange - Test nested transactions with multiple nesting levels var pool = CreatePool(maxPoolSize: 20); - const int threadCount = 15; - const int iterationsPerThread = 100; var tasks = new Task[threadCount]; var exceptions = new ConcurrentBag(); var successCount = 0; try { - // Act - Complete transaction before returning connection + // Act for (int t = 0; t < threadCount; t++) { tasks[t] = Task.Run(() => @@ -698,22 +666,7 @@ public void StressTest_CompleteBeforeReturn_ManyOperations() { for (int i = 0; i < iterationsPerThread; i++) { - DbConnectionInternal? conn = null; - SqlConnection? owner = null; - - using (var scope = new TransactionScope()) - { - owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); - Assert.NotNull(conn); - - // Complete transaction BEFORE returning - scope.Complete(); - } // Transaction completes here - - // Return connection AFTER transaction scope disposal - // TODO: questionable, make sure we're not double returning - pool.ReturnInternalConnection(conn!, owner!); + ExecuteNestedTransaction(pool, nestingLevel); Interlocked.Increment(ref successCount); } } @@ -737,19 +690,766 @@ public void StressTest_CompleteBeforeReturn_ManyOperations() } } + private void ExecuteNestedTransaction(WaitHandleDbConnectionPool pool, int nestingLevel) + { + if (nestingLevel <= 0) + { + return; + } + + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + + // Recursively create nested transaction + if (nestingLevel > 1) + { + ExecuteNestedTransaction(pool, nestingLevel - 1); + } + + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); + } + + [Theory] + [InlineData(5, 3, 10)] + public async Task StressTest_NestedTransactions_MultipleLevels_Async(int threadCount, int nestingLevel, int iterationsPerThread) + { + // Arrange - Test nested transactions with multiple nesting levels + var pool = CreatePool(maxPoolSize: 20); + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + var successCount = 0; + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(async () => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + await ExecuteNestedTransactionAsync(pool, nestingLevel); + Interlocked.Increment(ref successCount); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + await Task.WhenAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.Equal(threadCount * iterationsPerThread, successCount); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + private async Task ExecuteNestedTransactionAsync(WaitHandleDbConnectionPool pool, int nestingLevel) + { + if (nestingLevel <= 0) + { + return; + } + + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var transaction = Transaction.Current; + var owner = new SqlConnection(); + + var tcs = new TaskCompletionSource(transaction); + pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); + conn ??= await tcs.Task; + + Assert.NotNull(conn); + + // Recursively create nested transaction + if (nestingLevel > 1) + { + await ExecuteNestedTransactionAsync(pool, nestingLevel - 1); + } + + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); + } + + #endregion + + #region Distributed Transaction Tests + + [Fact] + public void StressTest_DistributedTransactions_MultipleConnections() + { + // Arrange - Test distributed transactions with multiple connections + var pool = CreatePool(maxPoolSize: 20); + const int threadCount = 5; + const int iterationsPerThread = 10; + const int connectionsPerTransaction = 3; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + var successCount = 0; + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var transaction = Transaction.Current; + Assert.NotNull(transaction); + + var connections = new List<(DbConnectionInternal conn, SqlConnection owner)>(); + + // Get multiple connections within the same distributed transaction + for (int c = 0; c < connectionsPerTransaction; c++) + { + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + connections.Add((conn, owner)); + } + + // Return all connections + foreach (var (conn, owner) in connections) + { + pool.ReturnInternalConnection(conn, owner); + } + + scope.Complete(); + Interlocked.Increment(ref successCount); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.Equal(threadCount * iterationsPerThread, successCount); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public async Task StressTest_DistributedTransactions_MultipleConnections_Async() + { + // Arrange - Test distributed transactions with multiple connections + var pool = CreatePool(maxPoolSize: 20); + const int threadCount = 5; + const int iterationsPerThread = 10; + const int connectionsPerTransaction = 3; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + var successCount = 0; + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(async () => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var transaction = Transaction.Current; + Assert.NotNull(transaction); + + var connections = new List<(DbConnectionInternal conn, SqlConnection owner)>(); + + // Get multiple connections within the same distributed transaction + for (int c = 0; c < connectionsPerTransaction; c++) + { + var owner = new SqlConnection(); + var tcs = new TaskCompletionSource(transaction); + pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); + conn ??= await tcs.Task; + + Assert.NotNull(conn); + connections.Add((conn, owner)); + } + + // Return all connections + foreach (var (conn, owner) in connections) + { + pool.ReturnInternalConnection(conn, owner); + } + + scope.Complete(); + Interlocked.Increment(ref successCount); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + await Task.WhenAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.Equal(threadCount * iterationsPerThread, successCount); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + #region Transaction Completion Race Condition Tests + + [Fact] + public void StressTest_RaceCondition_ReturnDuringTransactionCompletion() + { + // Arrange - Test race condition where connection is returned as transaction completes + var pool = CreatePool(maxPoolSize: 15); + const int threadCount = 10; + const int iterationsPerThread = 20; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + DbConnectionInternal? conn = null; + SqlConnection? owner = null; + + using (var scope = new TransactionScope()) + { + owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); + Assert.NotNull(conn); + + // Complete transaction + scope.Complete(); + } // Transaction is completing here + + // Try to return connection immediately after scope disposal + pool.ReturnInternalConnection(conn!, owner!); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public async Task StressTest_RaceCondition_ReturnDuringTransactionCompletion_Async() + { + // Arrange - Test race condition where connection is returned as transaction completes + var pool = CreatePool(maxPoolSize: 15); + const int threadCount = 10; + const int iterationsPerThread = 20; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(async () => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + DbConnectionInternal? conn = null; + SqlConnection? owner = null; + + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var transaction = Transaction.Current; + owner = new SqlConnection(); + + var tcs = new TaskCompletionSource(transaction); + pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out conn); + conn ??= await tcs.Task; + + Assert.NotNull(conn); + + // Complete transaction + scope.Complete(); + } // Transaction is completing here + + // Try to return connection immediately after scope disposal + pool.ReturnInternalConnection(conn!, owner!); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + await Task.WhenAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public void StressTest_RaceCondition_SimultaneousReturnAndCompletion() + { + // Arrange - Test race condition with very tight timing between return and completion + var pool = CreatePool(maxPoolSize: 10); + const int threadCount = 10; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + var returnTask = Task.CompletedTask; + DbConnectionInternal? conn = null; + SqlConnection? owner = null; + + using (var scope = new TransactionScope()) + { + owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); + Assert.NotNull(conn); + + // Start a task to return the connection on a different thread + var localConn = conn; + var localOwner = owner; + returnTask = Task.Run(() => + { + pool.ReturnInternalConnection(localConn, localOwner); + }); + + scope.Complete(); + } // Scope disposes here, potentially racing with returnTask + + // Wait for return task to complete + returnTask.Wait(); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + [Fact] + public async Task StressTest_RaceCondition_SimultaneousReturnAndCompletion_Async() + { + // Arrange - Test race condition with very tight timing between return and completion + var pool = CreatePool(maxPoolSize: 10); + const int threadCount = 10; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(async () => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + var returnTask = Task.CompletedTask; + DbConnectionInternal? conn = null; + SqlConnection? owner = null; + + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + var transaction = Transaction.Current; + owner = new SqlConnection(); + + var tcs = new TaskCompletionSource(transaction); + pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out conn); + conn ??= await tcs.Task; + + Assert.NotNull(conn); + + // Start a task to return the connection on a different thread + var localConn = conn; + var localOwner = owner; + returnTask = Task.Run(() => + { + pool.ReturnInternalConnection(localConn, localOwner); + }); + + scope.Complete(); + } // Scope disposes here, potentially racing with returnTask + + // Wait for return task to complete + await returnTask; + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + await Task.WhenAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + // TODO saturate pool with open transactions and verify waits time out as expected + // TODO test with nested transactions + // TODO test with distributed transactions + // TODO find a way to test the race conditions where a connection is returned just as a transaction is completing, this should strand the connection in the transacted pool. + + #region Intermingled Transaction Stress Tests + + [Fact] + public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() + { + // Arrange + var pool = CreatePool(maxPoolSize: 40); + const int threadCount = 20; + const int iterationsPerThread = 50; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + try + { + // Act - Half the threads use transactions, half don't + for (int t = 0; t < threadCount; t++) + { + bool useTransactions = t % 2 == 0; + tasks[t] = Task.Run(async () => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + if (useTransactions) + { + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); + } + else + { + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + pool.ReturnInternalConnection(conn, owner); + } + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + #endregion + + + [Fact] + public void StressTest_MaxPoolSaturation_WithTransactions() + { + // Arrange + var pool = CreatePool(maxPoolSize: 25); + const int threadCount = 50; // More threads than pool size + const int iterationsPerThread = 20; + var activeTasks = new ConcurrentBag(); + var exceptions = new ConcurrentBag(); + var barrier = new Barrier(threadCount); + + try + { + // Act - Saturate the pool with transactions + var tasks = Enumerable.Range(0, threadCount).Select(t => Task.Run(() => + { + try + { + // Synchronize all threads to start at once + + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + var obtained = pool.TryGetConnection( + owner, + null, + new DbConnectionOptions("Timeout=30", null), + out DbConnectionInternal? connection); + + if (obtained && connection != null) + { + // Hold connection briefly + pool.ReturnInternalConnection(connection, owner); + } + + scope.Complete(); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + })).ToArray(); + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + + [Fact] + public void StressTest_TransactionRollback_ManyOperations() + { + // Arrange + var pool = CreatePool(maxPoolSize: 20); + const int threadCount = 10; + const int iterationsPerThread = 100; + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + var rollbackCount = 0; + + try + { + // Act - Alternate between commit and rollback + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + for (int i = 0; i < iterationsPerThread; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + + // Randomly commit or rollback + if (i % 2 == 0) + { + scope.Complete(); + } + else + { + Interlocked.Increment(ref rollbackCount); + // Don't call Complete - let it rollback + } + + pool.ReturnInternalConnection(conn!, owner); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.True(rollbackCount > 0, "Expected some rollbacks"); + AssertPoolMetrics(pool); + } + finally + { + pool.Shutdown(); + } + } + + #region Edge Case Stress Tests + + [Fact] + public void StressTest_RapidPoolShutdownDuringTransactions() + { + // Arrange + var pool = CreatePool(maxPoolSize: 15); + const int threadCount = 20; + var barrier = new Barrier(threadCount); + var tasks = new Task[threadCount]; + var exceptions = new ConcurrentBag(); + + // Act + for (int t = 0; t < threadCount; t++) + { + tasks[t] = Task.Run(() => + { + try + { + barrier.SignalAndWait(); + + for (int i = 0; i < 50; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); + + var obtained = pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=5", null), out var conn); + + if (obtained && conn != null) + { + pool.ReturnInternalConnection(conn, owner); + } + + scope.Complete(); + } + } + catch (Exception ex) + { + // Some exceptions expected during shutdown + exceptions.Add(ex); + } + }); + } + + // Shutdown pool while operations are in progress + Thread.Sleep(100); + pool.Shutdown(); + + Task.WaitAll(tasks); + + // Assert - Just verify no crash occurred and pool count is valid + AssertPoolMetrics(pool); + } + + #endregion + + #region Transaction Completion Order Tests + [Fact] - public void StressTest_DelayedReturn_AfterTransactionDisposal() + public void StressTest_CompleteBeforeReturn_ManyOperations() { - // Arrange - Test returning connections with varying delays after transaction disposal + // Arrange - Test completing transaction before returning connection var pool = CreatePool(maxPoolSize: 20); const int threadCount = 15; - const int iterationsPerThread = 50; + const int iterationsPerThread = 100; var tasks = new Task[threadCount]; var exceptions = new ConcurrentBag(); + var successCount = 0; try { - // Act + // Act - Complete transaction before returning connection for (int t = 0; t < threadCount; t++) { tasks[t] = Task.Run(() => @@ -766,13 +1466,15 @@ public void StressTest_DelayedReturn_AfterTransactionDisposal() owner = new SqlConnection(); pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); Assert.NotNull(conn); - scope.Complete(); - } // Transaction disposed here - // Delay before returning (simulates held connection) - Thread.Sleep(s_random.Value!.Next(1, 10)); + // Complete transaction BEFORE returning + scope.Complete(); + } // Transaction completes here + // Return connection AFTER transaction scope disposal + // TODO: questionable, make sure we're not double returning pool.ReturnInternalConnection(conn!, owner!); + Interlocked.Increment(ref successCount); } } catch (Exception ex) @@ -786,6 +1488,7 @@ public void StressTest_DelayedReturn_AfterTransactionDisposal() // Assert Assert.Empty(exceptions); + Assert.Equal(threadCount * iterationsPerThread, successCount); AssertPoolMetrics(pool); } finally From 42c8fbdd1d43d937c5011e5a8250c607df3918c0 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Mon, 24 Nov 2025 10:58:21 -0800 Subject: [PATCH 14/18] Clean up new tests. --- ...leDbConnectionPoolTransactionStressTest.cs | 555 ++---------------- 1 file changed, 61 insertions(+), 494 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 67e4d71d10..a5f2191ca6 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -28,9 +28,6 @@ public class WaitHandleDbConnectionPoolTransactionStressTest private const int DefaultMinPoolSize = 0; private readonly int DefaultCreationTimeout = TimeSpan.FromSeconds(15).Milliseconds; - // Thread-safe random number generator for .NET Framework compatibility - private static readonly ThreadLocal s_random = new(() => new Random(Guid.NewGuid().GetHashCode())); - #region Helper Methods private WaitHandleDbConnectionPool CreatePool( @@ -94,7 +91,7 @@ public void StressTest_TransactionPerIteration(int threadCount, int iterationsPe // Act for (int t = 0; t < threadCount; t++) { - tasks[t] = Task.Run(async () => + tasks[t] = Task.Run(() => { for (int i = 0; i < iterationsPerThread; i++) { @@ -439,6 +436,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() { saturatingTasks[t] = Task.Run(async () => { + var signalled = false; try { using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); @@ -449,6 +447,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() // Signal that we've acquired a connection barrier.SignalAndWait(); + signalled = true; // Hold the connection briefly await Task.Delay(200); @@ -459,6 +458,11 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() catch (Exception ex) { exceptions.Add(ex); + if (!signalled) + { + // Ensure barrier is released even on exception + barrier.SignalAndWait(); + } } }); } @@ -476,11 +480,10 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() using var scope = new TransactionScope(); var owner = new SqlConnection(); - // Try to get connection with short timeout var obtained = pool.TryGetConnection( owner, null, - new DbConnectionOptions("Connection Timeout=1", null), + new DbConnectionOptions("", null), out var conn); if (!obtained || conn == null) @@ -506,10 +509,26 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() Assert.Empty(exceptions); Assert.True(completedWithoutConnection >= 0, $"Completed without connection: {completedWithoutConnection}"); + + // Act + // Now that everything is released, we should be able to get a connection again. + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); + + var tcs = new TaskCompletionSource(Transaction.Current); + var obtained = pool.TryGetConnection( + owner, + null, + new DbConnectionOptions("", null), + out var conn); + + // Assert + Assert.NotNull(conn); } finally { pool.Shutdown(); + pool.Clear(); } } @@ -591,7 +610,7 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A var obtained = pool.TryGetConnection( owner, tcs, - new DbConnectionOptions("Connection Timeout=1", null), + new DbConnectionOptions("", null), out var conn); if (!obtained) @@ -606,7 +625,7 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A } else { - conn = await tcs.Task; + conn = tcs.Task.Result; pool.ReturnInternalConnection(conn, owner); scope.Complete(); } @@ -634,6 +653,23 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A Assert.Empty(exceptions); Assert.True(completedWithoutConnection >= 0, $"Completed without connection: {completedWithoutConnection}"); + + // Act + // Now that everything is released, we should be able to get a connection again. + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); + + var tcs = new TaskCompletionSource(Transaction.Current); + var obtained = pool.TryGetConnection( + owner, + tcs, + new DbConnectionOptions("", null), + out var conn); + + conn ??= await tcs.Task; + + // Assert + Assert.NotNull(conn); } finally { @@ -646,8 +682,9 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A #region Nested Transaction Tests [Theory] - [InlineData(5, 3, 10)] - public void StressTest_NestedTransactions_MultipleLevels(int threadCount, int nestingLevel, int iterationsPerThread) + [InlineData(5, 3, 10, TransactionScopeOption.RequiresNew)] + [InlineData(5, 3, 10, TransactionScopeOption.Required)] + public void StressTest_NestedTransactions(int threadCount, int nestingLevel, int iterationsPerThread, TransactionScopeOption transactionScopeOption) { // Arrange - Test nested transactions with multiple nesting levels var pool = CreatePool(maxPoolSize: 20); @@ -666,7 +703,7 @@ public void StressTest_NestedTransactions_MultipleLevels(int threadCount, int ne { for (int i = 0; i < iterationsPerThread; i++) { - ExecuteNestedTransaction(pool, nestingLevel); + ExecuteNestedTransaction(pool, nestingLevel, transactionScopeOption); Interlocked.Increment(ref successCount); } } @@ -690,14 +727,14 @@ public void StressTest_NestedTransactions_MultipleLevels(int threadCount, int ne } } - private void ExecuteNestedTransaction(WaitHandleDbConnectionPool pool, int nestingLevel) + private void ExecuteNestedTransaction(WaitHandleDbConnectionPool pool, int nestingLevel, TransactionScopeOption transactionScopeOption) { if (nestingLevel <= 0) { return; } - using var scope = new TransactionScope(); + using var scope = new TransactionScope(transactionScopeOption); var owner = new SqlConnection(); pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); @@ -706,7 +743,7 @@ private void ExecuteNestedTransaction(WaitHandleDbConnectionPool pool, int nesti // Recursively create nested transaction if (nestingLevel > 1) { - ExecuteNestedTransaction(pool, nestingLevel - 1); + ExecuteNestedTransaction(pool, nestingLevel - 1, transactionScopeOption); } pool.ReturnInternalConnection(conn, owner); @@ -714,8 +751,9 @@ private void ExecuteNestedTransaction(WaitHandleDbConnectionPool pool, int nesti } [Theory] - [InlineData(5, 3, 10)] - public async Task StressTest_NestedTransactions_MultipleLevels_Async(int threadCount, int nestingLevel, int iterationsPerThread) + [InlineData(5, 3, 10, TransactionScopeOption.RequiresNew)] + [InlineData(5, 3, 10, TransactionScopeOption.Required)] + public async Task StressTest_NestedTransactions_Async(int threadCount, int nestingLevel, int iterationsPerThread, TransactionScopeOption transactionScopeOption) { // Arrange - Test nested transactions with multiple nesting levels var pool = CreatePool(maxPoolSize: 20); @@ -734,7 +772,7 @@ public async Task StressTest_NestedTransactions_MultipleLevels_Async(int threadC { for (int i = 0; i < iterationsPerThread; i++) { - await ExecuteNestedTransactionAsync(pool, nestingLevel); + await ExecuteNestedTransactionAsync(pool, nestingLevel, transactionScopeOption); Interlocked.Increment(ref successCount); } } @@ -758,14 +796,14 @@ public async Task StressTest_NestedTransactions_MultipleLevels_Async(int threadC } } - private async Task ExecuteNestedTransactionAsync(WaitHandleDbConnectionPool pool, int nestingLevel) + private async Task ExecuteNestedTransactionAsync(WaitHandleDbConnectionPool pool, int nestingLevel, TransactionScopeOption transactionScopeOption) { if (nestingLevel <= 0) { return; } - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + using var scope = new TransactionScope(transactionScopeOption, TransactionScopeAsyncFlowOption.Enabled); var transaction = Transaction.Current; var owner = new SqlConnection(); @@ -778,7 +816,7 @@ private async Task ExecuteNestedTransactionAsync(WaitHandleDbConnectionPool pool // Recursively create nested transaction if (nestingLevel > 1) { - await ExecuteNestedTransactionAsync(pool, nestingLevel - 1); + await ExecuteNestedTransactionAsync(pool, nestingLevel - 1, transactionScopeOption); } pool.ReturnInternalConnection(conn, owner); @@ -787,412 +825,6 @@ private async Task ExecuteNestedTransactionAsync(WaitHandleDbConnectionPool pool #endregion - #region Distributed Transaction Tests - - [Fact] - public void StressTest_DistributedTransactions_MultipleConnections() - { - // Arrange - Test distributed transactions with multiple connections - var pool = CreatePool(maxPoolSize: 20); - const int threadCount = 5; - const int iterationsPerThread = 10; - const int connectionsPerTransaction = 3; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - var successCount = 0; - - try - { - // Act - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(() => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(); - var transaction = Transaction.Current; - Assert.NotNull(transaction); - - var connections = new List<(DbConnectionInternal conn, SqlConnection owner)>(); - - // Get multiple connections within the same distributed transaction - for (int c = 0; c < connectionsPerTransaction; c++) - { - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - connections.Add((conn, owner)); - } - - // Return all connections - foreach (var (conn, owner) in connections) - { - pool.ReturnInternalConnection(conn, owner); - } - - scope.Complete(); - Interlocked.Increment(ref successCount); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - [Fact] - public async Task StressTest_DistributedTransactions_MultipleConnections_Async() - { - // Arrange - Test distributed transactions with multiple connections - var pool = CreatePool(maxPoolSize: 20); - const int threadCount = 5; - const int iterationsPerThread = 10; - const int connectionsPerTransaction = 3; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - var successCount = 0; - - try - { - // Act - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(async () => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var transaction = Transaction.Current; - Assert.NotNull(transaction); - - var connections = new List<(DbConnectionInternal conn, SqlConnection owner)>(); - - // Get multiple connections within the same distributed transaction - for (int c = 0; c < connectionsPerTransaction; c++) - { - var owner = new SqlConnection(); - var tcs = new TaskCompletionSource(transaction); - pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); - conn ??= await tcs.Task; - - Assert.NotNull(conn); - connections.Add((conn, owner)); - } - - // Return all connections - foreach (var (conn, owner) in connections) - { - pool.ReturnInternalConnection(conn, owner); - } - - scope.Complete(); - Interlocked.Increment(ref successCount); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - await Task.WhenAll(tasks); - - // Assert - Assert.Empty(exceptions); - Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - #endregion - - #region Transaction Completion Race Condition Tests - - [Fact] - public void StressTest_RaceCondition_ReturnDuringTransactionCompletion() - { - // Arrange - Test race condition where connection is returned as transaction completes - var pool = CreatePool(maxPoolSize: 15); - const int threadCount = 10; - const int iterationsPerThread = 20; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - - try - { - // Act - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(() => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - DbConnectionInternal? conn = null; - SqlConnection? owner = null; - - using (var scope = new TransactionScope()) - { - owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); - Assert.NotNull(conn); - - // Complete transaction - scope.Complete(); - } // Transaction is completing here - - // Try to return connection immediately after scope disposal - pool.ReturnInternalConnection(conn!, owner!); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - [Fact] - public async Task StressTest_RaceCondition_ReturnDuringTransactionCompletion_Async() - { - // Arrange - Test race condition where connection is returned as transaction completes - var pool = CreatePool(maxPoolSize: 15); - const int threadCount = 10; - const int iterationsPerThread = 20; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - - try - { - // Act - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(async () => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - DbConnectionInternal? conn = null; - SqlConnection? owner = null; - - using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) - { - var transaction = Transaction.Current; - owner = new SqlConnection(); - - var tcs = new TaskCompletionSource(transaction); - pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out conn); - conn ??= await tcs.Task; - - Assert.NotNull(conn); - - // Complete transaction - scope.Complete(); - } // Transaction is completing here - - // Try to return connection immediately after scope disposal - pool.ReturnInternalConnection(conn!, owner!); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - await Task.WhenAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - [Fact] - public void StressTest_RaceCondition_SimultaneousReturnAndCompletion() - { - // Arrange - Test race condition with very tight timing between return and completion - var pool = CreatePool(maxPoolSize: 10); - const int threadCount = 10; - const int iterationsPerThread = 50; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - - try - { - // Act - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(() => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - var returnTask = Task.CompletedTask; - DbConnectionInternal? conn = null; - SqlConnection? owner = null; - - using (var scope = new TransactionScope()) - { - owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); - Assert.NotNull(conn); - - // Start a task to return the connection on a different thread - var localConn = conn; - var localOwner = owner; - returnTask = Task.Run(() => - { - pool.ReturnInternalConnection(localConn, localOwner); - }); - - scope.Complete(); - } // Scope disposes here, potentially racing with returnTask - - // Wait for return task to complete - returnTask.Wait(); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - [Fact] - public async Task StressTest_RaceCondition_SimultaneousReturnAndCompletion_Async() - { - // Arrange - Test race condition with very tight timing between return and completion - var pool = CreatePool(maxPoolSize: 10); - const int threadCount = 10; - const int iterationsPerThread = 50; - var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); - - try - { - // Act - for (int t = 0; t < threadCount; t++) - { - tasks[t] = Task.Run(async () => - { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - var returnTask = Task.CompletedTask; - DbConnectionInternal? conn = null; - SqlConnection? owner = null; - - using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) - { - var transaction = Transaction.Current; - owner = new SqlConnection(); - - var tcs = new TaskCompletionSource(transaction); - pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out conn); - conn ??= await tcs.Task; - - Assert.NotNull(conn); - - // Start a task to return the connection on a different thread - var localConn = conn; - var localOwner = owner; - returnTask = Task.Run(() => - { - pool.ReturnInternalConnection(localConn, localOwner); - }); - - scope.Complete(); - } // Scope disposes here, potentially racing with returnTask - - // Wait for return task to complete - await returnTask; - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } - - await Task.WhenAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - - #endregion - - // TODO saturate pool with open transactions and verify waits time out as expected - // TODO test with nested transactions - // TODO test with distributed transactions - // TODO find a way to test the race conditions where a connection is returned just as a transaction is completing, this should strand the connection in the transacted pool. - #region Intermingled Transaction Stress Tests [Fact] @@ -1256,65 +888,7 @@ public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() #endregion - - [Fact] - public void StressTest_MaxPoolSaturation_WithTransactions() - { - // Arrange - var pool = CreatePool(maxPoolSize: 25); - const int threadCount = 50; // More threads than pool size - const int iterationsPerThread = 20; - var activeTasks = new ConcurrentBag(); - var exceptions = new ConcurrentBag(); - var barrier = new Barrier(threadCount); - - try - { - // Act - Saturate the pool with transactions - var tasks = Enumerable.Range(0, threadCount).Select(t => Task.Run(() => - { - try - { - // Synchronize all threads to start at once - - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); - - var obtained = pool.TryGetConnection( - owner, - null, - new DbConnectionOptions("Timeout=30", null), - out DbConnectionInternal? connection); - - if (obtained && connection != null) - { - // Hold connection briefly - pool.ReturnInternalConnection(connection, owner); - } - - scope.Complete(); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - })).ToArray(); - - Task.WaitAll(tasks); - - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } - } - + #region Edge Case Stress Tests [Fact] public void StressTest_TransactionRollback_ManyOperations() @@ -1378,10 +952,8 @@ public void StressTest_TransactionRollback_ManyOperations() } } - #region Edge Case Stress Tests - [Fact] - public void StressTest_RapidPoolShutdownDuringTransactions() + public void StressTest_PoolShutdownDuringTransactions() { // Arrange var pool = CreatePool(maxPoolSize: 15); @@ -1423,7 +995,6 @@ public void StressTest_RapidPoolShutdownDuringTransactions() } // Shutdown pool while operations are in progress - Thread.Sleep(100); pool.Shutdown(); Task.WaitAll(tasks); @@ -1432,12 +1003,8 @@ public void StressTest_RapidPoolShutdownDuringTransactions() AssertPoolMetrics(pool); } - #endregion - - #region Transaction Completion Order Tests - [Fact] - public void StressTest_CompleteBeforeReturn_ManyOperations() + public void StressTest_TransactionCompleteBeforeReturn() { // Arrange - Test completing transaction before returning connection var pool = CreatePool(maxPoolSize: 20); From 1f31bd279ad46347383ac885485512363c974d1a Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Mon, 24 Nov 2025 12:24:16 -0800 Subject: [PATCH 15/18] Improve cleanup --- ...leDbConnectionPoolTransactionStressTest.cs | 1058 ++++++++--------- 1 file changed, 484 insertions(+), 574 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index a5f2191ca6..80427f92c9 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -22,12 +22,20 @@ namespace Microsoft.Data.SqlClient.UnitTests.ConnectionPool; /// These tests verify that pool metrics remain consistent when connections are rapidly opened and closed /// with intermingled transactions in a highly concurrent environment. /// -public class WaitHandleDbConnectionPoolTransactionStressTest +public class WaitHandleDbConnectionPoolTransactionStressTest : IDisposable { private const int DefaultMaxPoolSize = 50; private const int DefaultMinPoolSize = 0; private readonly int DefaultCreationTimeout = TimeSpan.FromSeconds(15).Milliseconds; + private WaitHandleDbConnectionPool? pool; + + public void Dispose() + { + pool?.Shutdown(); + pool?.Clear(); + } + #region Helper Methods private WaitHandleDbConnectionPool CreatePool( @@ -80,101 +88,81 @@ private void AssertPoolMetrics(WaitHandleDbConnectionPool pool) [InlineData(10, 100)] public void StressTest_TransactionPerIteration(int threadCount, int iterationsPerThread) { - // Tests many threads and iterations, with each iteration creating a transaction scope. - // Arrange - var pool = CreatePool(); + pool = CreatePool(); var tasks = new Task[threadCount]; - try + // Act + for (int t = 0; t < threadCount; t++) { - // Act - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(() => { - tasks[t] = Task.Run(() => + for (int i = 0; i < iterationsPerThread; i++) { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); - var obtained = pool.TryGetConnection( - owner, - taskCompletionSource: null, - new DbConnectionOptions("", null), - out DbConnectionInternal? connection); + var obtained = pool.TryGetConnection( + owner, + taskCompletionSource: null, + new DbConnectionOptions("", null), + out DbConnectionInternal? connection); - Assert.True(obtained); - Assert.NotNull(connection); + Assert.True(obtained); + Assert.NotNull(connection); - pool.ReturnInternalConnection(connection, owner); - scope.Complete(); - } - }); - } + pool.ReturnInternalConnection(connection, owner); + scope.Complete(); + } + }); + } - Task.WaitAll(tasks); + Task.WaitAll(tasks); - // Assert - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - pool.Clear(); - } + // Assert + AssertPoolMetrics(pool); } [Theory] [InlineData(10, 100)] public async Task StressTest_TransactionPerIteration_Async(int threadCount, int iterationsPerThread) { - // Tests many threads and iterations, with each iteration creating a transaction scope. - // Arrange - var pool = CreatePool(); + pool = CreatePool(); var tasks = new Task[threadCount]; - try + // Act + for (int t = 0; t < threadCount; t++) { - // Act - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(async () => { - tasks[t] = Task.Run(async () => + for (int i = 0; i < iterationsPerThread; i++) { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); - var tcs = new TaskCompletionSource(); - var obtained = pool.TryGetConnection( - owner, - taskCompletionSource: tcs, - new DbConnectionOptions("", null), - out DbConnectionInternal? connection); + var tcs = new TaskCompletionSource(); + var obtained = pool.TryGetConnection( + owner, + taskCompletionSource: tcs, + new DbConnectionOptions("", null), + out DbConnectionInternal? connection); - // Wait for the task if not obtained immediately - connection ??= await tcs.Task; + // Wait for the task if not obtained immediately + connection ??= await tcs.Task; - Assert.NotNull(connection); + Assert.NotNull(connection); - pool.ReturnInternalConnection(connection, owner); - scope.Complete(); - } - }); - } + pool.ReturnInternalConnection(connection, owner); + scope.Complete(); + } + }); + } - await Task.WhenAll(tasks); + await Task.WhenAll(tasks); - // Assert - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - pool.Clear(); - } + // Assert + AssertPoolMetrics(pool); } [Theory] @@ -182,44 +170,37 @@ public async Task StressTest_TransactionPerIteration_Async(int threadCount, int public void StressTest_TransactionPerThread(int threadCount, int iterationsPerThread) { // Arrange - var pool = CreatePool(); + pool = CreatePool(); var tasks = new Task[threadCount]; - try + // Act - Each transaction should be isolated + for (int t = 0; t < threadCount; t++) { - // Act - Each transaction should be isolated - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(() => { - tasks[t] = Task.Run(() => - { - using var scope = new TransactionScope(); - var transaction = Transaction.Current; - Assert.NotNull(transaction); + using var scope = new TransactionScope(); + var transaction = Transaction.Current; + Assert.NotNull(transaction); - // Get multiple connections within same transaction - for (int i = 0; i < iterationsPerThread; i++) - { - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - pool.ReturnInternalConnection(conn, owner); - } + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) + { + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + pool.ReturnInternalConnection(conn, owner); + } - Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); + Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); - scope.Complete(); - }); - } + scope.Complete(); + }); + } - Task.WaitAll(tasks); + Task.WaitAll(tasks); - // Assert - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } + // Assert + AssertPoolMetrics(pool); } [Theory] @@ -227,57 +208,50 @@ public void StressTest_TransactionPerThread(int threadCount, int iterationsPerTh public async Task StressTest_TransactionPerThread_Async(int threadCount, int iterationsPerThread) { // Arrange - var pool = CreatePool(); + pool = CreatePool(); var tasks = new Task[threadCount]; - try + // Act - Each transaction should be isolated + for (int t = 0; t < threadCount; t++) { - // Act - Each transaction should be isolated - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(async () => { - tasks[t] = Task.Run(async () => - { - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var transaction = Transaction.Current; - Assert.NotNull(transaction); - - // Get multiple connections within same transaction - for (int i = 0; i < iterationsPerThread; i++) - { - var owner = new SqlConnection(); - // The transaction *must* be set as the AsyncState of the TaskCompletionSource. - // The - var tcs = new TaskCompletionSource(transaction); - pool.TryGetConnection( - owner, - tcs, - new DbConnectionOptions("", null), - out var conn); + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var transaction = Transaction.Current; + Assert.NotNull(transaction); - conn ??= await tcs.Task; + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) + { + var owner = new SqlConnection(); + // The transaction *must* be set as the AsyncState of the TaskCompletionSource. + // The + var tcs = new TaskCompletionSource(transaction); + pool.TryGetConnection( + owner, + tcs, + new DbConnectionOptions("", null), + out var conn); - Assert.NotNull(conn); + conn ??= await tcs.Task; - pool.ReturnInternalConnection(conn, owner); + Assert.NotNull(conn); - Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); - } + pool.ReturnInternalConnection(conn, owner); Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); + } - scope.Complete(); - }); - } - - await Task.WhenAll(tasks); + Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); - // Assert - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); + scope.Complete(); + }); } + + await Task.WhenAll(tasks); + + // Assert + AssertPoolMetrics(pool); } [Theory] @@ -285,63 +259,56 @@ public async Task StressTest_TransactionPerThread_Async(int threadCount, int ite public void StressTest_SingleSharedTransaction(int threadCount, int iterationsPerThread) { // Arrange - var pool = CreatePool(); + pool = CreatePool(); var tasks = new Task[threadCount]; - try + Transaction? transaction = null; + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { - Transaction? transaction = null; - using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + transaction = Transaction.Current; + Assert.NotNull(transaction); + // Act - Each transaction should be isolated + for (int t = 0; t < threadCount; t++) { - transaction = Transaction.Current; - Assert.NotNull(transaction); - // Act - Each transaction should be isolated - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(() => { - tasks[t] = Task.Run(() => + using (var innerScope = new TransactionScope(transaction)) { - using (var innerScope = new TransactionScope(transaction)) + Assert.Equal(transaction, Transaction.Current); + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) { - Assert.Equal(transaction, Transaction.Current); - // Get multiple connections within same transaction - for (int i = 0; i < iterationsPerThread; i++) - { - using var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - - // We bypass the SqlConnection.Open flow, so SqlConnection.InnerConnection is never set - // Therefore, SqlConnection.Close doesn't return the connection to the pool, we have to - // do it manually. - pool.ReturnInternalConnection(conn, owner); - } - - innerScope.Complete(); - } - }); - } + using var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); - Task.WaitAll(tasks); + // We bypass the SqlConnection.Open flow, so SqlConnection.InnerConnection is never set + // Therefore, SqlConnection.Close doesn't return the connection to the pool, we have to + // do it manually. + pool.ReturnInternalConnection(conn, owner); + } - //Console.WriteLine($"{pool.TransactedConnectionPool.TransactedConnections[transaction].Count} transacted connections"); - scope.Complete(); + innerScope.Complete(); + } + }); } - while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction!) - && pool.TransactedConnectionPool.TransactedConnections[transaction!].Count > 0) - { - // Wait for transaction to be cleaned up - Console.WriteLine("Waiting for transaction cleanup..."); - Thread.Sleep(100); - } + Task.WaitAll(tasks); - // Assert - AssertPoolMetrics(pool); + //Console.WriteLine($"{pool.TransactedConnectionPool.TransactedConnections[transaction].Count} transacted connections"); + scope.Complete(); } - finally + + while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction!) + && pool.TransactedConnectionPool.TransactedConnections[transaction!].Count > 0) { - pool.Shutdown(); + // Wait for transaction to be cleaned up + Console.WriteLine("Waiting for transaction cleanup..."); + Thread.Sleep(100); } + + // Assert + AssertPoolMetrics(pool); } [Theory] @@ -349,68 +316,61 @@ public void StressTest_SingleSharedTransaction(int threadCount, int iterationsPe public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int iterationsPerThread) { // Arrange - var pool = CreatePool(); + pool = CreatePool(); var tasks = new Task[threadCount]; - try + Transaction? transaction = null; + using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { - Transaction? transaction = null; - using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + transaction = Transaction.Current; + Assert.NotNull(transaction); + // Act - Each transaction should be isolated + for (int t = 0; t < threadCount; t++) { - transaction = Transaction.Current; - Assert.NotNull(transaction); - // Act - Each transaction should be isolated - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(async () => { - tasks[t] = Task.Run(async () => + using (var innerScope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) { - using (var innerScope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) + Assert.Equal(transaction, Transaction.Current); + // Get multiple connections within same transaction + for (int i = 0; i < iterationsPerThread; i++) { - Assert.Equal(transaction, Transaction.Current); - // Get multiple connections within same transaction - for (int i = 0; i < iterationsPerThread; i++) - { - var owner = new SqlConnection(); - // The transaction *must* be set as the AsyncState of the TaskCompletionSource. - // The - var tcs = new TaskCompletionSource(transaction); - pool.TryGetConnection( - owner, - tcs, - new DbConnectionOptions("", null), - out var conn); - - conn ??= await tcs.Task; - - Assert.NotNull(conn); - - pool.ReturnInternalConnection(conn, owner); - } - - innerScope.Complete(); - } - }); - } + var owner = new SqlConnection(); + // The transaction *must* be set as the AsyncState of the TaskCompletionSource. + // The + var tcs = new TaskCompletionSource(transaction); + pool.TryGetConnection( + owner, + tcs, + new DbConnectionOptions("", null), + out var conn); - //await Task.WhenAll(tasks); - scope.Complete(); - } + conn ??= await tcs.Task; - while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction!) - && pool.TransactedConnectionPool.TransactedConnections[transaction!].Count > 0) - { - // Wait for transaction to be cleaned up - Console.WriteLine("Waiting for transaction cleanup..."); - await Task.Delay(100); + Assert.NotNull(conn); + + pool.ReturnInternalConnection(conn, owner); + } + + innerScope.Complete(); + } + }); } - // Assert - AssertPoolMetrics(pool); + //await Task.WhenAll(tasks); + scope.Complete(); } - finally + + while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction!) + && pool.TransactedConnectionPool.TransactedConnections[transaction!].Count > 0) { - pool.Shutdown(); + // Wait for transaction to be cleaned up + Console.WriteLine("Waiting for transaction cleanup..."); + await Task.Delay(100); } + + // Assert + AssertPoolMetrics(pool); } #endregion @@ -420,7 +380,7 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() { // Arrange - Test that when pool is saturated with transactions, new requests behave correctly - var pool = CreatePool(maxPoolSize: 3); + pool = CreatePool(maxPoolSize: 3); const int saturatingThreadCount = 3; const int waitingThreadCount = 5; var saturatingTasks = new Task[saturatingThreadCount]; @@ -429,114 +389,106 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() var completedWithoutConnection = 0; var barrier = new Barrier(saturatingThreadCount + 1); - try + // Act - Saturate the pool with long-held connections in transactions + for (int t = 0; t < saturatingThreadCount; t++) { - // Act - Saturate the pool with long-held connections in transactions - for (int t = 0; t < saturatingThreadCount; t++) + saturatingTasks[t] = Task.Run(async () => { - saturatingTasks[t] = Task.Run(async () => + var signalled = false; + try { - var signalled = false; - try - { - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); - // Signal that we've acquired a connection - barrier.SignalAndWait(); - signalled = true; + // Signal that we've acquired a connection + barrier.SignalAndWait(); + signalled = true; - // Hold the connection briefly - await Task.Delay(200); + // Hold the connection briefly + await Task.Delay(200); - pool.ReturnInternalConnection(conn, owner); - scope.Complete(); - } - catch (Exception ex) + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); + } + catch (Exception ex) + { + exceptions.Add(ex); + if (!signalled) { - exceptions.Add(ex); - if (!signalled) - { - // Ensure barrier is released even on exception - barrier.SignalAndWait(); - } + // Ensure barrier is released even on exception + barrier.SignalAndWait(); } - }); - } + } + }); + } - // Wait for all saturating threads to acquire connections - barrier.SignalAndWait(); + // Wait for all saturating threads to acquire connections + barrier.SignalAndWait(); - // Now try to get more connections - pool is saturated - for (int t = 0; t < waitingThreadCount; t++) + // Now try to get more connections - pool is saturated + for (int t = 0; t < waitingThreadCount; t++) + { + waitingTasks[t] = Task.Run(() => { - waitingTasks[t] = Task.Run(() => + try { - try - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); + using var scope = new TransactionScope(); + var owner = new SqlConnection(); - var obtained = pool.TryGetConnection( - owner, - null, - new DbConnectionOptions("", null), - out var conn); + var obtained = pool.TryGetConnection( + owner, + null, + new DbConnectionOptions("", null), + out var conn); - if (!obtained || conn == null) - { - Interlocked.Increment(ref completedWithoutConnection); - } - else - { - pool.ReturnInternalConnection(conn, owner); - scope.Complete(); - } + if (!obtained || conn == null) + { + Interlocked.Increment(ref completedWithoutConnection); } - catch (Exception ex) + else { - exceptions.Add(ex); + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); } - }); - } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } - Task.WaitAll(saturatingTasks.Concat(waitingTasks).ToArray()); + Task.WaitAll(saturatingTasks.Concat(waitingTasks).ToArray()); - // Assert - Assert.Empty(exceptions); - Assert.True(completedWithoutConnection >= 0, - $"Completed without connection: {completedWithoutConnection}"); + // Assert + Assert.Empty(exceptions); + Assert.True(completedWithoutConnection >= 0, + $"Completed without connection: {completedWithoutConnection}"); - // Act - // Now that everything is released, we should be able to get a connection again. - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); + // Act + // Now that everything is released, we should be able to get a connection again. + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); - var tcs = new TaskCompletionSource(Transaction.Current); - var obtained = pool.TryGetConnection( - owner, - null, - new DbConnectionOptions("", null), - out var conn); + var tcs = new TaskCompletionSource(Transaction.Current); + var obtained = pool.TryGetConnection( + owner, + null, + new DbConnectionOptions("", null), + out var conn); - // Assert - Assert.NotNull(conn); - } - finally - { - pool.Shutdown(); - pool.Clear(); - } + // Assert + Assert.NotNull(conn); } [Fact] public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_Async() { // Arrange - Test that when pool is saturated with transactions, new requests behave correctly - var pool = CreatePool(maxPoolSize: 3); + pool = CreatePool(maxPoolSize: 3); const int saturatingThreadCount = 3; const int waitingThreadCount = 5; var saturatingTasks = new Task[saturatingThreadCount]; @@ -548,133 +500,126 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A var allSaturatingThreadsReady = new TaskCompletionSource(); var readyCount = 0; - try + // Act - Saturate the pool with long-held connections in transactions + for (int t = 0; t < saturatingThreadCount; t++) { - // Act - Saturate the pool with long-held connections in transactions - for (int t = 0; t < saturatingThreadCount; t++) + saturatingTasks[t] = Task.Run(async () => { - saturatingTasks[t] = Task.Run(async () => + try { - try - { - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); - var tcs = new TaskCompletionSource(Transaction.Current); - pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); - conn ??= await tcs.Task; + var tcs = new TaskCompletionSource(Transaction.Current); + pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); + conn ??= await tcs.Task; - Assert.NotNull(conn); + Assert.NotNull(conn); - // Signal that we've acquired a connection - if (Interlocked.Increment(ref readyCount) == saturatingThreadCount) - { - allSaturatingThreadsReady.TrySetResult(true); - } + // Signal that we've acquired a connection + if (Interlocked.Increment(ref readyCount) == saturatingThreadCount) + { + allSaturatingThreadsReady.TrySetResult(true); + } - // Wait for all saturating threads to be ready - await allSaturatingThreadsReady.Task; + // Wait for all saturating threads to be ready + await allSaturatingThreadsReady.Task; - // Hold the connection briefly - await Task.Delay(200); + // Hold the connection briefly + await Task.Delay(200); - pool.ReturnInternalConnection(conn, owner); - scope.Complete(); - } - catch (Exception ex) + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); + } + catch (Exception ex) + { + exceptions.Add(ex); + // Ensure barrier is released even on exception + if (Interlocked.Increment(ref readyCount) == saturatingThreadCount) { - exceptions.Add(ex); - // Ensure barrier is released even on exception - if (Interlocked.Increment(ref readyCount) == saturatingThreadCount) - { - allSaturatingThreadsReady.TrySetResult(true); - } + allSaturatingThreadsReady.TrySetResult(true); } - }); - } + } + }); + } - // Wait for all saturating threads to acquire connections - await allSaturatingThreadsReady.Task; + // Wait for all saturating threads to acquire connections + await allSaturatingThreadsReady.Task; - // Now start waiting threads - for (int t = 0; t < waitingThreadCount; t++) + // Now start waiting threads + for (int t = 0; t < waitingThreadCount; t++) + { + waitingTasks[t] = Task.Run(async () => { - waitingTasks[t] = Task.Run(async () => + try { - try - { - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); - var tcs = new TaskCompletionSource(Transaction.Current); - var obtained = pool.TryGetConnection( - owner, - tcs, - new DbConnectionOptions("", null), - out var conn); + var tcs = new TaskCompletionSource(Transaction.Current); + var obtained = pool.TryGetConnection( + owner, + tcs, + new DbConnectionOptions("", null), + out var conn); - if (!obtained) - { - // Try to wait with timeout - var timeoutTask = Task.Delay(300); - var completedTask = await Task.WhenAny(tcs.Task, timeoutTask); + if (!obtained) + { + // Try to wait with timeout + var timeoutTask = Task.Delay(300); + var completedTask = await Task.WhenAny(tcs.Task, timeoutTask); - if (completedTask == timeoutTask) - { - Interlocked.Increment(ref completedWithoutConnection); - } - else - { - conn = tcs.Task.Result; - pool.ReturnInternalConnection(conn, owner); - scope.Complete(); - } - } - else if (conn != null) + if (completedTask == timeoutTask) { - pool.ReturnInternalConnection(conn, owner); - scope.Complete(); + Interlocked.Increment(ref completedWithoutConnection); } else { - Interlocked.Increment(ref completedWithoutConnection); + conn = tcs.Task.Result; + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); } } - catch (Exception ex) + else if (conn != null) { - exceptions.Add(ex); + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); } - }); - } + else + { + Interlocked.Increment(ref completedWithoutConnection); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } - await Task.WhenAll(saturatingTasks.Concat(waitingTasks).ToArray()); + await Task.WhenAll(saturatingTasks.Concat(waitingTasks).ToArray()); - // Assert - Assert.Empty(exceptions); - Assert.True(completedWithoutConnection >= 0, - $"Completed without connection: {completedWithoutConnection}"); + // Assert + Assert.Empty(exceptions); + Assert.True(completedWithoutConnection >= 0, + $"Completed without connection: {completedWithoutConnection}"); - // Act - // Now that everything is released, we should be able to get a connection again. - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); + // Act + // Now that everything is released, we should be able to get a connection again. + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); - var tcs = new TaskCompletionSource(Transaction.Current); - var obtained = pool.TryGetConnection( - owner, - tcs, - new DbConnectionOptions("", null), - out var conn); + var tcs = new TaskCompletionSource(Transaction.Current); + var obtained = pool.TryGetConnection( + owner, + tcs, + new DbConnectionOptions("", null), + out var conn); - conn ??= await tcs.Task; + conn ??= await tcs.Task; - // Assert - Assert.NotNull(conn); - } - finally - { - pool.Shutdown(); - } + // Assert + Assert.NotNull(conn); } #endregion @@ -687,44 +632,37 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A public void StressTest_NestedTransactions(int threadCount, int nestingLevel, int iterationsPerThread, TransactionScopeOption transactionScopeOption) { // Arrange - Test nested transactions with multiple nesting levels - var pool = CreatePool(maxPoolSize: 20); + pool = CreatePool(maxPoolSize: 20); var tasks = new Task[threadCount]; var exceptions = new ConcurrentBag(); var successCount = 0; - try + // Act + for (int t = 0; t < threadCount; t++) { - // Act - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(() => { - tasks[t] = Task.Run(() => + try { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - ExecuteNestedTransaction(pool, nestingLevel, transactionScopeOption); - Interlocked.Increment(ref successCount); - } - } - catch (Exception ex) + for (int i = 0; i < iterationsPerThread; i++) { - exceptions.Add(ex); + ExecuteNestedTransaction(pool, nestingLevel, transactionScopeOption); + Interlocked.Increment(ref successCount); } - }); - } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } - Task.WaitAll(tasks); + Task.WaitAll(tasks); - // Assert - Assert.Empty(exceptions); - Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } + // Assert + Assert.Empty(exceptions); + Assert.Equal(threadCount * iterationsPerThread, successCount); + AssertPoolMetrics(pool); } private void ExecuteNestedTransaction(WaitHandleDbConnectionPool pool, int nestingLevel, TransactionScopeOption transactionScopeOption) @@ -756,44 +694,37 @@ private void ExecuteNestedTransaction(WaitHandleDbConnectionPool pool, int nesti public async Task StressTest_NestedTransactions_Async(int threadCount, int nestingLevel, int iterationsPerThread, TransactionScopeOption transactionScopeOption) { // Arrange - Test nested transactions with multiple nesting levels - var pool = CreatePool(maxPoolSize: 20); + pool = CreatePool(maxPoolSize: 20); var tasks = new Task[threadCount]; var exceptions = new ConcurrentBag(); var successCount = 0; - try + // Act + for (int t = 0; t < threadCount; t++) { - // Act - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(async () => { - tasks[t] = Task.Run(async () => + try { - try - { - for (int i = 0; i < iterationsPerThread; i++) - { - await ExecuteNestedTransactionAsync(pool, nestingLevel, transactionScopeOption); - Interlocked.Increment(ref successCount); - } - } - catch (Exception ex) + for (int i = 0; i < iterationsPerThread; i++) { - exceptions.Add(ex); + await ExecuteNestedTransactionAsync(pool, nestingLevel, transactionScopeOption); + Interlocked.Increment(ref successCount); } - }); - } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } - await Task.WhenAll(tasks); + await Task.WhenAll(tasks); - // Assert - Assert.Empty(exceptions); - Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } + // Assert + Assert.Empty(exceptions); + Assert.Equal(threadCount * iterationsPerThread, successCount); + AssertPoolMetrics(pool); } private async Task ExecuteNestedTransactionAsync(WaitHandleDbConnectionPool pool, int nestingLevel, TransactionScopeOption transactionScopeOption) @@ -831,59 +762,52 @@ private async Task ExecuteNestedTransactionAsync(WaitHandleDbConnectionPool pool public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() { // Arrange - var pool = CreatePool(maxPoolSize: 40); + pool = CreatePool(maxPoolSize: 40); const int threadCount = 20; const int iterationsPerThread = 50; var tasks = new Task[threadCount]; var exceptions = new ConcurrentBag(); - try + // Act - Half the threads use transactions, half don't + for (int t = 0; t < threadCount; t++) { - // Act - Half the threads use transactions, half don't - for (int t = 0; t < threadCount; t++) + bool useTransactions = t % 2 == 0; + tasks[t] = Task.Run(() => { - bool useTransactions = t % 2 == 0; - tasks[t] = Task.Run(async () => + try { - try + for (int i = 0; i < iterationsPerThread; i++) { - for (int i = 0; i < iterationsPerThread; i++) + if (useTransactions) { - if (useTransactions) - { - using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - pool.ReturnInternalConnection(conn, owner); - scope.Complete(); - } - else - { - var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); - pool.ReturnInternalConnection(conn, owner); - } + using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + pool.ReturnInternalConnection(conn, owner); + scope.Complete(); + } + else + { + var owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); + pool.ReturnInternalConnection(conn, owner); } } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } - Task.WaitAll(tasks); + Task.WaitAll(tasks); - // Assert - Assert.Empty(exceptions); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } + // Assert + Assert.Empty(exceptions); + AssertPoolMetrics(pool); } #endregion @@ -894,69 +818,62 @@ public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() public void StressTest_TransactionRollback_ManyOperations() { // Arrange - var pool = CreatePool(maxPoolSize: 20); + pool = CreatePool(maxPoolSize: 20); const int threadCount = 10; const int iterationsPerThread = 100; var tasks = new Task[threadCount]; var exceptions = new ConcurrentBag(); var rollbackCount = 0; - try + // Act - Alternate between commit and rollback + for (int t = 0; t < threadCount; t++) { - // Act - Alternate between commit and rollback - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(() => { - tasks[t] = Task.Run(() => + try { - try + for (int i = 0; i < iterationsPerThread; i++) { - for (int i = 0; i < iterationsPerThread; i++) - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); + using var scope = new TransactionScope(); + var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - Assert.NotNull(conn); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + Assert.NotNull(conn); - // Randomly commit or rollback - if (i % 2 == 0) - { - scope.Complete(); - } - else - { - Interlocked.Increment(ref rollbackCount); - // Don't call Complete - let it rollback - } - - pool.ReturnInternalConnection(conn!, owner); + // Randomly commit or rollback + if (i % 2 == 0) + { + scope.Complete(); } + else + { + Interlocked.Increment(ref rollbackCount); + // Don't call Complete - let it rollback + } + + pool.ReturnInternalConnection(conn!, owner); } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); + } - Task.WaitAll(tasks); + Task.WaitAll(tasks); - // Assert - Assert.Empty(exceptions); - Assert.True(rollbackCount > 0, "Expected some rollbacks"); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); - } + // Assert + Assert.Empty(exceptions); + Assert.True(rollbackCount > 0, "Expected some rollbacks"); + AssertPoolMetrics(pool); } [Fact] public void StressTest_PoolShutdownDuringTransactions() { // Arrange - var pool = CreatePool(maxPoolSize: 15); + pool = CreatePool(maxPoolSize: 15); const int threadCount = 20; var barrier = new Barrier(threadCount); var tasks = new Task[threadCount]; @@ -1007,61 +924,54 @@ public void StressTest_PoolShutdownDuringTransactions() public void StressTest_TransactionCompleteBeforeReturn() { // Arrange - Test completing transaction before returning connection - var pool = CreatePool(maxPoolSize: 20); + pool = CreatePool(maxPoolSize: 20); const int threadCount = 15; const int iterationsPerThread = 100; var tasks = new Task[threadCount]; var exceptions = new ConcurrentBag(); var successCount = 0; - try + // Act - Complete transaction before returning connection + for (int t = 0; t < threadCount; t++) { - // Act - Complete transaction before returning connection - for (int t = 0; t < threadCount; t++) + tasks[t] = Task.Run(() => { - tasks[t] = Task.Run(() => + try { - try + for (int i = 0; i < iterationsPerThread; i++) { - for (int i = 0; i < iterationsPerThread; i++) + DbConnectionInternal? conn = null; + SqlConnection? owner = null; + + using (var scope = new TransactionScope()) { - DbConnectionInternal? conn = null; - SqlConnection? owner = null; - - using (var scope = new TransactionScope()) - { - owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); - Assert.NotNull(conn); - - // Complete transaction BEFORE returning - scope.Complete(); - } // Transaction completes here - - // Return connection AFTER transaction scope disposal - // TODO: questionable, make sure we're not double returning - pool.ReturnInternalConnection(conn!, owner!); - Interlocked.Increment(ref successCount); - } - } - catch (Exception ex) - { - exceptions.Add(ex); - } - }); - } + owner = new SqlConnection(); + pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); + Assert.NotNull(conn); - Task.WaitAll(tasks); + // Complete transaction BEFORE returning + scope.Complete(); + } // Transaction completes here - // Assert - Assert.Empty(exceptions); - Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool); - } - finally - { - pool.Shutdown(); + // Return connection AFTER transaction scope disposal + // TODO: questionable, make sure we're not double returning + pool.ReturnInternalConnection(conn!, owner!); + Interlocked.Increment(ref successCount); + } + } + catch (Exception ex) + { + exceptions.Add(ex); + } + }); } + + Task.WaitAll(tasks); + + // Assert + Assert.Empty(exceptions); + Assert.Equal(threadCount * iterationsPerThread, successCount); + AssertPoolMetrics(pool); } #endregion From 90f268697ae1c641bed517c7e42e57f097b02aaf Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Mon, 24 Nov 2025 14:20:05 -0800 Subject: [PATCH 16/18] Fix copilot issues. Add doc comment. --- .../ConnectionPool/DbConnectionPoolOptions.cs | 3 + ...leDbConnectionPoolTransactionStressTest.cs | 97 ++++++++----------- 2 files changed, 44 insertions(+), 56 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolOptions.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolOptions.cs index 7adf8abdc2..5ac6f4d565 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolOptions.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolOptions.cs @@ -39,6 +39,9 @@ bool hasTransactionAffinity _hasTransactionAffinity = hasTransactionAffinity; } + /// + /// The time (in milliseconds) to wait for a connection to be created/returned before terminating the attempt. + /// public int CreationTimeout { get { return _creationTimeout; } diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 80427f92c9..14e3f1d26c 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -26,7 +26,7 @@ public class WaitHandleDbConnectionPoolTransactionStressTest : IDisposable { private const int DefaultMaxPoolSize = 50; private const int DefaultMinPoolSize = 0; - private readonly int DefaultCreationTimeout = TimeSpan.FromSeconds(15).Milliseconds; + private readonly int DefaultCreationTimeoutInMilliseconds = 15000; private WaitHandleDbConnectionPool? pool; @@ -47,7 +47,7 @@ private WaitHandleDbConnectionPool CreatePool( poolByIdentity: false, minPoolSize: minPoolSize, maxPoolSize: maxPoolSize, - creationTimeout: DefaultCreationTimeout, + creationTimeout: DefaultCreationTimeoutInMilliseconds, loadBalanceTimeout: 0, hasTransactionAffinity: hasTransactionAffinity ); @@ -102,13 +102,12 @@ public void StressTest_TransactionPerIteration(int threadCount, int iterationsPe using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var owner = new SqlConnection(); - var obtained = pool.TryGetConnection( + pool.TryGetConnection( owner, taskCompletionSource: null, new DbConnectionOptions("", null), out DbConnectionInternal? connection); - Assert.True(obtained); Assert.NotNull(connection); pool.ReturnInternalConnection(connection, owner); @@ -142,7 +141,7 @@ public async Task StressTest_TransactionPerIteration_Async(int threadCount, int var owner = new SqlConnection(); var tcs = new TaskCompletionSource(); - var obtained = pool.TryGetConnection( + pool.TryGetConnection( owner, taskCompletionSource: tcs, new DbConnectionOptions("", null), @@ -225,7 +224,6 @@ public async Task StressTest_TransactionPerThread_Async(int threadCount, int ite { var owner = new SqlConnection(); // The transaction *must* be set as the AsyncState of the TaskCompletionSource. - // The var tcs = new TaskCompletionSource(transaction); pool.TryGetConnection( owner, @@ -295,18 +293,9 @@ public void StressTest_SingleSharedTransaction(int threadCount, int iterationsPe Task.WaitAll(tasks); - //Console.WriteLine($"{pool.TransactedConnectionPool.TransactedConnections[transaction].Count} transacted connections"); scope.Complete(); } - while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction!) - && pool.TransactedConnectionPool.TransactedConnections[transaction!].Count > 0) - { - // Wait for transaction to be cleaned up - Console.WriteLine("Waiting for transaction cleanup..."); - Thread.Sleep(100); - } - // Assert AssertPoolMetrics(pool); } @@ -329,7 +318,9 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int { tasks[t] = Task.Run(async () => { - using (var innerScope = new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled)) + using (var innerScope = new TransactionScope( + transaction, + TransactionScopeAsyncFlowOption.Enabled)) { Assert.Equal(transaction, Transaction.Current); // Get multiple connections within same transaction @@ -337,7 +328,6 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int { var owner = new SqlConnection(); // The transaction *must* be set as the AsyncState of the TaskCompletionSource. - // The var tcs = new TaskCompletionSource(transaction); pool.TryGetConnection( owner, @@ -357,18 +347,10 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int }); } - //await Task.WhenAll(tasks); + await Task.WhenAll(tasks); scope.Complete(); } - while (pool.TransactedConnectionPool.TransactedConnections.ContainsKey(transaction!) - && pool.TransactedConnectionPool.TransactedConnections[transaction!].Count > 0) - { - // Wait for transaction to be cleaned up - Console.WriteLine("Waiting for transaction cleanup..."); - await Task.Delay(100); - } - // Assert AssertPoolMetrics(pool); } @@ -387,7 +369,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() var waitingTasks = new Task[waitingThreadCount]; var exceptions = new ConcurrentBag(); var completedWithoutConnection = 0; - var barrier = new Barrier(saturatingThreadCount + 1); + using var barrier = new Barrier(saturatingThreadCount + 1); // Act - Saturate the pool with long-held connections in transactions for (int t = 0; t < saturatingThreadCount; t++) @@ -473,8 +455,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var owner = new SqlConnection(); - var tcs = new TaskCompletionSource(Transaction.Current); - var obtained = pool.TryGetConnection( + pool.TryGetConnection( owner, null, new DbConnectionOptions("", null), @@ -610,7 +591,7 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A var owner = new SqlConnection(); var tcs = new TaskCompletionSource(Transaction.Current); - var obtained = pool.TryGetConnection( + pool.TryGetConnection( owner, tcs, new DbConnectionOptions("", null), @@ -629,7 +610,11 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A [Theory] [InlineData(5, 3, 10, TransactionScopeOption.RequiresNew)] [InlineData(5, 3, 10, TransactionScopeOption.Required)] - public void StressTest_NestedTransactions(int threadCount, int nestingLevel, int iterationsPerThread, TransactionScopeOption transactionScopeOption) + public void StressTest_NestedTransactions( + int threadCount, + int nestingLevel, + int iterationsPerThread, + TransactionScopeOption transactionScopeOption) { // Arrange - Test nested transactions with multiple nesting levels pool = CreatePool(maxPoolSize: 20); @@ -665,7 +650,10 @@ public void StressTest_NestedTransactions(int threadCount, int nestingLevel, int AssertPoolMetrics(pool); } - private void ExecuteNestedTransaction(WaitHandleDbConnectionPool pool, int nestingLevel, TransactionScopeOption transactionScopeOption) + private void ExecuteNestedTransaction( + WaitHandleDbConnectionPool pool, + int nestingLevel, + TransactionScopeOption transactionScopeOption) { if (nestingLevel <= 0) { @@ -691,7 +679,11 @@ private void ExecuteNestedTransaction(WaitHandleDbConnectionPool pool, int nesti [Theory] [InlineData(5, 3, 10, TransactionScopeOption.RequiresNew)] [InlineData(5, 3, 10, TransactionScopeOption.Required)] - public async Task StressTest_NestedTransactions_Async(int threadCount, int nestingLevel, int iterationsPerThread, TransactionScopeOption transactionScopeOption) + public async Task StressTest_NestedTransactions_Async( + int threadCount, + int nestingLevel, + int iterationsPerThread, + TransactionScopeOption transactionScopeOption) { // Arrange - Test nested transactions with multiple nesting levels pool = CreatePool(maxPoolSize: 20); @@ -727,7 +719,10 @@ public async Task StressTest_NestedTransactions_Async(int threadCount, int nesti AssertPoolMetrics(pool); } - private async Task ExecuteNestedTransactionAsync(WaitHandleDbConnectionPool pool, int nestingLevel, TransactionScopeOption transactionScopeOption) + private async Task ExecuteNestedTransactionAsync( + WaitHandleDbConnectionPool pool, + int nestingLevel, + TransactionScopeOption transactionScopeOption) { if (nestingLevel <= 0) { @@ -875,38 +870,29 @@ public void StressTest_PoolShutdownDuringTransactions() // Arrange pool = CreatePool(maxPoolSize: 15); const int threadCount = 20; - var barrier = new Barrier(threadCount); + using var barrier = new Barrier(threadCount); var tasks = new Task[threadCount]; - var exceptions = new ConcurrentBag(); // Act for (int t = 0; t < threadCount; t++) { tasks[t] = Task.Run(() => { - try - { - barrier.SignalAndWait(); + barrier.SignalAndWait(); - for (int i = 0; i < 50; i++) - { - using var scope = new TransactionScope(); - var owner = new SqlConnection(); + for (int i = 0; i < 50; i++) + { + using var scope = new TransactionScope(); + var owner = new SqlConnection(); - var obtained = pool.TryGetConnection(owner, null, new DbConnectionOptions("Timeout=5", null), out var conn); + pool.TryGetConnection(owner, + null, + new DbConnectionOptions("", null), + out var conn); - if (obtained && conn != null) - { - pool.ReturnInternalConnection(conn, owner); - } + pool.ReturnInternalConnection(conn, owner); - scope.Complete(); - } - } - catch (Exception ex) - { - // Some exceptions expected during shutdown - exceptions.Add(ex); + scope.Complete(); } }); } @@ -954,7 +940,6 @@ public void StressTest_TransactionCompleteBeforeReturn() } // Transaction completes here // Return connection AFTER transaction scope disposal - // TODO: questionable, make sure we're not double returning pool.ReturnInternalConnection(conn!, owner!); Interlocked.Increment(ref successCount); } From 861b0e74bf1ddfce500e71efb5d5a245c75a8e02 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Mon, 24 Nov 2025 15:09:48 -0800 Subject: [PATCH 17/18] Address copilot comments. --- .../ConnectionPool/IDbConnectionPool.cs | 7 +- .../TransactedConnectionPool.cs | 2 +- .../WaitHandleDbConnectionPool.cs | 2 +- ...leDbConnectionPoolTransactionStressTest.cs | 149 +++++++++--------- 4 files changed, 82 insertions(+), 78 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs index b684bb24bb..bfc5789d3f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/IDbConnectionPool.cs @@ -85,6 +85,11 @@ internal interface IDbConnectionPool /// DbConnectionPoolState State { get; } + /// + /// Holds connections that are currently enlisted in a transaction. + /// + TransactedConnectionPool TransactedConnectionPool { get; } + /// /// Indicates whether the connection pool is using load balancing. /// @@ -106,7 +111,7 @@ internal interface IDbConnectionPool /// The user options to use if a new connection must be opened. /// The retrieved connection will be passed out via this parameter. /// True if a connection was set in the out parameter, otherwise returns false. - bool TryGetConnection(DbConnection owningObject, TaskCompletionSource taskCompletionSource, DbConnectionOptions userOptions, out DbConnectionInternal? connection); + bool TryGetConnection(DbConnection owningObject, TaskCompletionSource? taskCompletionSource, DbConnectionOptions userOptions, out DbConnectionInternal? connection); /// /// Replaces the internal connection currently associated with owningObject with a new internal connection from the pool. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs index 6c94daa69f..14ec847379 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/TransactedConnectionPool.cs @@ -349,4 +349,4 @@ internal void TransactionEnded(Transaction transaction, DbConnectionInternal tra } #endregion -} \ No newline at end of file +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs index cc7009884b..1508a0d41f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs @@ -328,7 +328,7 @@ public bool IsRunning private bool UsingIntegrateSecurity => _identity != null && DbConnectionPoolIdentity.NoIdentity != _identity; - internal TransactedConnectionPool TransactedConnectionPool => _transactedConnectionPool; + public TransactedConnectionPool TransactedConnectionPool => _transactedConnectionPool; private void CleanupCallback(object state) { diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 14e3f1d26c..940ad6e631 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Data.Common; using System.Linq; using System.Threading; @@ -28,12 +27,12 @@ public class WaitHandleDbConnectionPoolTransactionStressTest : IDisposable private const int DefaultMinPoolSize = 0; private readonly int DefaultCreationTimeoutInMilliseconds = 15000; - private WaitHandleDbConnectionPool? pool; + private IDbConnectionPool? _pool; public void Dispose() { - pool?.Shutdown(); - pool?.Clear(); + _pool?.Shutdown(); + _pool?.Clear(); } #region Helper Methods @@ -71,11 +70,11 @@ private WaitHandleDbConnectionPool CreatePool( return pool; } - private void AssertPoolMetrics(WaitHandleDbConnectionPool pool) + private void AssertPoolMetrics(IDbConnectionPool pool) { - Assert.True(pool.Count <= pool.MaxPoolSize, - $"Pool count ({pool.Count}) exceeded max pool size ({pool.MaxPoolSize})"); - Assert.True(pool.Count >= 0, + Assert.True(pool.Count <= pool.PoolGroupOptions.MaxPoolSize, + $"Pool count ({pool.Count}) exceeded max pool size ({pool.PoolGroupOptions.MaxPoolSize})"); + Assert.True(pool.Count >= pool.PoolGroupOptions.MinPoolSize, $"Pool count ({pool.Count}) is negative"); Assert.Empty(pool.TransactedConnectionPool.TransactedConnections); } @@ -89,7 +88,7 @@ private void AssertPoolMetrics(WaitHandleDbConnectionPool pool) public void StressTest_TransactionPerIteration(int threadCount, int iterationsPerThread) { // Arrange - pool = CreatePool(); + _pool = CreatePool(); var tasks = new Task[threadCount]; // Act @@ -102,7 +101,7 @@ public void StressTest_TransactionPerIteration(int threadCount, int iterationsPe using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var owner = new SqlConnection(); - pool.TryGetConnection( + _pool.TryGetConnection( owner, taskCompletionSource: null, new DbConnectionOptions("", null), @@ -110,7 +109,7 @@ public void StressTest_TransactionPerIteration(int threadCount, int iterationsPe Assert.NotNull(connection); - pool.ReturnInternalConnection(connection, owner); + _pool.ReturnInternalConnection(connection, owner); scope.Complete(); } }); @@ -119,7 +118,7 @@ public void StressTest_TransactionPerIteration(int threadCount, int iterationsPe Task.WaitAll(tasks); // Assert - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } [Theory] @@ -127,7 +126,7 @@ public void StressTest_TransactionPerIteration(int threadCount, int iterationsPe public async Task StressTest_TransactionPerIteration_Async(int threadCount, int iterationsPerThread) { // Arrange - pool = CreatePool(); + _pool = CreatePool(); var tasks = new Task[threadCount]; // Act @@ -141,7 +140,7 @@ public async Task StressTest_TransactionPerIteration_Async(int threadCount, int var owner = new SqlConnection(); var tcs = new TaskCompletionSource(); - pool.TryGetConnection( + _pool.TryGetConnection( owner, taskCompletionSource: tcs, new DbConnectionOptions("", null), @@ -152,7 +151,7 @@ public async Task StressTest_TransactionPerIteration_Async(int threadCount, int Assert.NotNull(connection); - pool.ReturnInternalConnection(connection, owner); + _pool.ReturnInternalConnection(connection, owner); scope.Complete(); } }); @@ -161,7 +160,7 @@ public async Task StressTest_TransactionPerIteration_Async(int threadCount, int await Task.WhenAll(tasks); // Assert - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } [Theory] @@ -169,7 +168,7 @@ public async Task StressTest_TransactionPerIteration_Async(int threadCount, int public void StressTest_TransactionPerThread(int threadCount, int iterationsPerThread) { // Arrange - pool = CreatePool(); + _pool = CreatePool(); var tasks = new Task[threadCount]; // Act - Each transaction should be isolated @@ -185,12 +184,12 @@ public void StressTest_TransactionPerThread(int threadCount, int iterationsPerTh for (int i = 0; i < iterationsPerThread; i++) { var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + _pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); Assert.NotNull(conn); - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); } - Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); + Assert.Single(_pool.TransactedConnectionPool.TransactedConnections[transaction]); scope.Complete(); }); @@ -199,7 +198,7 @@ public void StressTest_TransactionPerThread(int threadCount, int iterationsPerTh Task.WaitAll(tasks); // Assert - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } [Theory] @@ -207,7 +206,7 @@ public void StressTest_TransactionPerThread(int threadCount, int iterationsPerTh public async Task StressTest_TransactionPerThread_Async(int threadCount, int iterationsPerThread) { // Arrange - pool = CreatePool(); + _pool = CreatePool(); var tasks = new Task[threadCount]; // Act - Each transaction should be isolated @@ -225,7 +224,7 @@ public async Task StressTest_TransactionPerThread_Async(int threadCount, int ite var owner = new SqlConnection(); // The transaction *must* be set as the AsyncState of the TaskCompletionSource. var tcs = new TaskCompletionSource(transaction); - pool.TryGetConnection( + _pool.TryGetConnection( owner, tcs, new DbConnectionOptions("", null), @@ -235,12 +234,12 @@ public async Task StressTest_TransactionPerThread_Async(int threadCount, int ite Assert.NotNull(conn); - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); - Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); + Assert.Single(_pool.TransactedConnectionPool.TransactedConnections[transaction]); } - Assert.Single(pool.TransactedConnectionPool.TransactedConnections[transaction]); + Assert.Single(_pool.TransactedConnectionPool.TransactedConnections[transaction]); scope.Complete(); }); @@ -249,7 +248,7 @@ public async Task StressTest_TransactionPerThread_Async(int threadCount, int ite await Task.WhenAll(tasks); // Assert - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } [Theory] @@ -257,7 +256,7 @@ public async Task StressTest_TransactionPerThread_Async(int threadCount, int ite public void StressTest_SingleSharedTransaction(int threadCount, int iterationsPerThread) { // Arrange - pool = CreatePool(); + _pool = CreatePool(); var tasks = new Task[threadCount]; Transaction? transaction = null; @@ -277,13 +276,13 @@ public void StressTest_SingleSharedTransaction(int threadCount, int iterationsPe for (int i = 0; i < iterationsPerThread; i++) { using var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + _pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); Assert.NotNull(conn); // We bypass the SqlConnection.Open flow, so SqlConnection.InnerConnection is never set // Therefore, SqlConnection.Close doesn't return the connection to the pool, we have to // do it manually. - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); } innerScope.Complete(); @@ -297,7 +296,7 @@ public void StressTest_SingleSharedTransaction(int threadCount, int iterationsPe } // Assert - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } [Theory] @@ -305,7 +304,7 @@ public void StressTest_SingleSharedTransaction(int threadCount, int iterationsPe public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int iterationsPerThread) { // Arrange - pool = CreatePool(); + _pool = CreatePool(); var tasks = new Task[threadCount]; Transaction? transaction = null; @@ -329,7 +328,7 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int var owner = new SqlConnection(); // The transaction *must* be set as the AsyncState of the TaskCompletionSource. var tcs = new TaskCompletionSource(transaction); - pool.TryGetConnection( + _pool.TryGetConnection( owner, tcs, new DbConnectionOptions("", null), @@ -339,7 +338,7 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int Assert.NotNull(conn); - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); } innerScope.Complete(); @@ -352,7 +351,7 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int } // Assert - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } #endregion @@ -362,7 +361,7 @@ public async Task StressTest_SingleSharedTransaction_Async(int threadCount, int public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() { // Arrange - Test that when pool is saturated with transactions, new requests behave correctly - pool = CreatePool(maxPoolSize: 3); + _pool = CreatePool(maxPoolSize: 3); const int saturatingThreadCount = 3; const int waitingThreadCount = 5; var saturatingTasks = new Task[saturatingThreadCount]; @@ -382,7 +381,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + _pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); Assert.NotNull(conn); // Signal that we've acquired a connection @@ -392,7 +391,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() // Hold the connection briefly await Task.Delay(200); - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); scope.Complete(); } catch (Exception ex) @@ -420,7 +419,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() using var scope = new TransactionScope(); var owner = new SqlConnection(); - var obtained = pool.TryGetConnection( + var obtained = _pool.TryGetConnection( owner, null, new DbConnectionOptions("", null), @@ -432,7 +431,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() } else { - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); scope.Complete(); } } @@ -455,7 +454,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var owner = new SqlConnection(); - pool.TryGetConnection( + _pool.TryGetConnection( owner, null, new DbConnectionOptions("", null), @@ -469,7 +468,7 @@ public void StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout() public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_Async() { // Arrange - Test that when pool is saturated with transactions, new requests behave correctly - pool = CreatePool(maxPoolSize: 3); + _pool = CreatePool(maxPoolSize: 3); const int saturatingThreadCount = 3; const int waitingThreadCount = 5; var saturatingTasks = new Task[saturatingThreadCount]; @@ -492,7 +491,7 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A var owner = new SqlConnection(); var tcs = new TaskCompletionSource(Transaction.Current); - pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); + _pool.TryGetConnection(owner, tcs, new DbConnectionOptions("", null), out var conn); conn ??= await tcs.Task; Assert.NotNull(conn); @@ -509,7 +508,7 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A // Hold the connection briefly await Task.Delay(200); - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); scope.Complete(); } catch (Exception ex) @@ -538,7 +537,7 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A var owner = new SqlConnection(); var tcs = new TaskCompletionSource(Transaction.Current); - var obtained = pool.TryGetConnection( + var obtained = _pool.TryGetConnection( owner, tcs, new DbConnectionOptions("", null), @@ -557,13 +556,13 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A else { conn = tcs.Task.Result; - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); scope.Complete(); } } else if (conn != null) { - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); scope.Complete(); } else @@ -591,7 +590,7 @@ public async Task StressTest_PoolSaturation_WithOpenTransactions_VerifyTimeout_A var owner = new SqlConnection(); var tcs = new TaskCompletionSource(Transaction.Current); - pool.TryGetConnection( + _pool.TryGetConnection( owner, tcs, new DbConnectionOptions("", null), @@ -617,7 +616,7 @@ public void StressTest_NestedTransactions( TransactionScopeOption transactionScopeOption) { // Arrange - Test nested transactions with multiple nesting levels - pool = CreatePool(maxPoolSize: 20); + _pool = CreatePool(maxPoolSize: 20); var tasks = new Task[threadCount]; var exceptions = new ConcurrentBag(); var successCount = 0; @@ -631,7 +630,7 @@ public void StressTest_NestedTransactions( { for (int i = 0; i < iterationsPerThread; i++) { - ExecuteNestedTransaction(pool, nestingLevel, transactionScopeOption); + ExecuteNestedTransaction(_pool, nestingLevel, transactionScopeOption); Interlocked.Increment(ref successCount); } } @@ -647,11 +646,11 @@ public void StressTest_NestedTransactions( // Assert Assert.Empty(exceptions); Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } private void ExecuteNestedTransaction( - WaitHandleDbConnectionPool pool, + IDbConnectionPool pool, int nestingLevel, TransactionScopeOption transactionScopeOption) { @@ -686,7 +685,7 @@ public async Task StressTest_NestedTransactions_Async( TransactionScopeOption transactionScopeOption) { // Arrange - Test nested transactions with multiple nesting levels - pool = CreatePool(maxPoolSize: 20); + _pool = CreatePool(maxPoolSize: 20); var tasks = new Task[threadCount]; var exceptions = new ConcurrentBag(); var successCount = 0; @@ -700,7 +699,7 @@ public async Task StressTest_NestedTransactions_Async( { for (int i = 0; i < iterationsPerThread; i++) { - await ExecuteNestedTransactionAsync(pool, nestingLevel, transactionScopeOption); + await ExecuteNestedTransactionAsync(_pool, nestingLevel, transactionScopeOption); Interlocked.Increment(ref successCount); } } @@ -716,11 +715,11 @@ public async Task StressTest_NestedTransactions_Async( // Assert Assert.Empty(exceptions); Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } private async Task ExecuteNestedTransactionAsync( - WaitHandleDbConnectionPool pool, + IDbConnectionPool pool, int nestingLevel, TransactionScopeOption transactionScopeOption) { @@ -757,7 +756,7 @@ private async Task ExecuteNestedTransactionAsync( public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() { // Arrange - pool = CreatePool(maxPoolSize: 40); + _pool = CreatePool(maxPoolSize: 40); const int threadCount = 20; const int iterationsPerThread = 50; var tasks = new Task[threadCount]; @@ -777,17 +776,17 @@ public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() { using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + _pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); Assert.NotNull(conn); - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); scope.Complete(); } else { var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + _pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); Assert.NotNull(conn); - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn, owner); } } } @@ -802,7 +801,7 @@ public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() // Assert Assert.Empty(exceptions); - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } #endregion @@ -813,7 +812,7 @@ public void StressTest_MixedTransactedAndNonTransacted_HighConcurrency() public void StressTest_TransactionRollback_ManyOperations() { // Arrange - pool = CreatePool(maxPoolSize: 20); + _pool = CreatePool(maxPoolSize: 20); const int threadCount = 10; const int iterationsPerThread = 100; var tasks = new Task[threadCount]; @@ -832,7 +831,7 @@ public void StressTest_TransactionRollback_ManyOperations() using var scope = new TransactionScope(); var owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); + _pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); Assert.NotNull(conn); // Randomly commit or rollback @@ -846,7 +845,7 @@ public void StressTest_TransactionRollback_ManyOperations() // Don't call Complete - let it rollback } - pool.ReturnInternalConnection(conn!, owner); + _pool.ReturnInternalConnection(conn!, owner); } } catch (Exception ex) @@ -861,14 +860,14 @@ public void StressTest_TransactionRollback_ManyOperations() // Assert Assert.Empty(exceptions); Assert.True(rollbackCount > 0, "Expected some rollbacks"); - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } [Fact] public void StressTest_PoolShutdownDuringTransactions() { // Arrange - pool = CreatePool(maxPoolSize: 15); + _pool = CreatePool(maxPoolSize: 15); const int threadCount = 20; using var barrier = new Barrier(threadCount); var tasks = new Task[threadCount]; @@ -885,12 +884,12 @@ public void StressTest_PoolShutdownDuringTransactions() using var scope = new TransactionScope(); var owner = new SqlConnection(); - pool.TryGetConnection(owner, + _pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out var conn); - pool.ReturnInternalConnection(conn, owner); + _pool.ReturnInternalConnection(conn!, owner); scope.Complete(); } @@ -898,19 +897,19 @@ public void StressTest_PoolShutdownDuringTransactions() } // Shutdown pool while operations are in progress - pool.Shutdown(); + _pool.Shutdown(); Task.WaitAll(tasks); // Assert - Just verify no crash occurred and pool count is valid - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } [Fact] public void StressTest_TransactionCompleteBeforeReturn() { // Arrange - Test completing transaction before returning connection - pool = CreatePool(maxPoolSize: 20); + _pool = CreatePool(maxPoolSize: 20); const int threadCount = 15; const int iterationsPerThread = 100; var tasks = new Task[threadCount]; @@ -932,7 +931,7 @@ public void StressTest_TransactionCompleteBeforeReturn() using (var scope = new TransactionScope()) { owner = new SqlConnection(); - pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); + _pool.TryGetConnection(owner, null, new DbConnectionOptions("", null), out conn); Assert.NotNull(conn); // Complete transaction BEFORE returning @@ -940,7 +939,7 @@ public void StressTest_TransactionCompleteBeforeReturn() } // Transaction completes here // Return connection AFTER transaction scope disposal - pool.ReturnInternalConnection(conn!, owner!); + _pool.ReturnInternalConnection(conn!, owner!); Interlocked.Increment(ref successCount); } } @@ -956,7 +955,7 @@ public void StressTest_TransactionCompleteBeforeReturn() // Assert Assert.Empty(exceptions); Assert.Equal(threadCount * iterationsPerThread, successCount); - AssertPoolMetrics(pool); + AssertPoolMetrics(_pool); } #endregion From 017353cf853b4e043e4aa9f4e22b9e9e71f24526 Mon Sep 17 00:00:00 2001 From: Malcolm Daigle Date: Mon, 24 Nov 2025 15:23:08 -0800 Subject: [PATCH 18/18] Fix tests. Expose TransactedConnectionPool. --- .../Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs | 4 ++++ .../UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs | 5 +++-- .../WaitHandleDbConnectionPoolTransactionStressTest.cs | 5 ++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs index 62c11705be..398918c301 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/ChannelDbConnectionPool.cs @@ -95,6 +95,7 @@ internal ChannelDbConnectionPool( Identity = identity; AuthenticationContexts = new(); MaxPoolSize = Convert.ToUInt32(PoolGroupOptions.MaxPoolSize); + TransactedConnectionPool = new(this); _connectionSlots = new(MaxPoolSize); @@ -147,6 +148,9 @@ public ConcurrentDictionary< /// public DbConnectionPoolState State { get; private set; } + /// + public TransactedConnectionPool TransactedConnectionPool { get; } + /// public bool UseLoadBalancing => PoolGroupOptions.UseLoadBalancing; diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs index 3fc31338cd..18bd9c5ea3 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/TransactedConnectionPoolTest.cs @@ -668,13 +668,14 @@ internal class MockDbConnectionPool : IDbConnectionPool public DbConnectionPoolGroupOptions PoolGroupOptions => throw new NotImplementedException(); public DbConnectionPoolProviderInfo ProviderInfo => throw new NotImplementedException(); public DbConnectionPoolState State => throw new NotImplementedException(); + public TransactedConnectionPool TransactedConnectionPool => throw new NotImplementedException(); public bool UseLoadBalancing => throw new NotImplementedException(); public ConcurrentBag ReturnedConnections { get; } = new(); public void Clear() => throw new NotImplementedException(); - public bool TryGetConnection(DbConnection owningObject, TaskCompletionSource taskCompletionSource, DbConnectionOptions userOptions, out DbConnectionInternal? connection) + public bool TryGetConnection(DbConnection owningObject, TaskCompletionSource? taskCompletionSource, DbConnectionOptions userOptions, out DbConnectionInternal? connection) { throw new NotImplementedException(); } @@ -739,4 +740,4 @@ internal override void ResetConnection() } #endregion -} \ No newline at end of file +} diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs index 940ad6e631..1a9dc9e36f 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/ConnectionPool/WaitHandleDbConnectionPoolTransactionStressTest.cs @@ -889,7 +889,10 @@ public void StressTest_PoolShutdownDuringTransactions() new DbConnectionOptions("", null), out var conn); - _pool.ReturnInternalConnection(conn!, owner); + if (conn is not null) + { + _pool.ReturnInternalConnection(conn, owner); + } scope.Complete(); }