From 1c671db26e8dc8732bd71175b78695a6b7971f35 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 23 Sep 2025 15:02:07 -0700 Subject: [PATCH 1/7] Fix connection pool concurrency issue caused due to waiting on multiple semaphore handles, instead of only creation handle during pool create request. --- .../ConnectionPool/WaitHandleDbConnectionPool.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 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 4cef28fd78..b8852657b2 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 @@ -1506,7 +1506,9 @@ private void PoolCreateRequest(object state) { // Obtain creation mutex so we're the only one creating objects // and we must have the wait result - waitResult = WaitHandle.WaitAny(_waitHandles.GetHandles(withCreate: true), CreationTimeout); + bool creationHandleObtained = _waitHandles.CreationSemaphore.WaitOne(CreationTimeout); + waitResult = creationHandleObtained ? CREATION_HANDLE : WaitHandle.WaitTimeout; + if (CREATION_HANDLE == waitResult) { DbConnectionInternal newObj; @@ -1542,17 +1544,12 @@ private void PoolCreateRequest(object state) } } } - else if (WaitHandle.WaitTimeout == waitResult) + else { // do not wait forever and potential block this worker thread // instead wait for a period of time and just requeue to try again QueuePoolCreateRequest(); } - else - { - // trace waitResult and ignore the failure - SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, PoolCreateRequest called WaitForSingleObject failed {1}", Id, waitResult); - } } catch (Exception e) { From 4e9ffea14b1ecf2cbf21427fd389c39412bfec4e Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 1 Oct 2025 16:32:08 -0700 Subject: [PATCH 2/7] Address feedback, manage semaphore with a semaphoreHolder --- .../WaitHandleDbConnectionPool.cs | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 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 b8852657b2..224baed0ff 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 @@ -383,6 +383,41 @@ internal WaitHandle[] GetHandles(bool withCreate) } } + /// + /// Helper class to obtain and release a semaphore. + /// + internal class SemaphoreHolder : IDisposable + { + private readonly Semaphore _semaphore; + + /// + /// Whether the semaphore was successfully obtained within the timeout. + /// + internal bool Obtained { get; private set; } + + /// + /// Obtains the semaphore, waiting up to the specified timeout. + /// + /// + /// + internal SemaphoreHolder(Semaphore semaphore, int timeout) + { + _semaphore = semaphore; + Obtained = _semaphore.WaitOne(timeout); + } + + /// + /// Releases the semaphore if it was successfully obtained. + /// + public void Dispose() + { + if (Obtained) + { + _semaphore.Release(1); + } + } + } + private const int MAX_Q_SIZE = 0x00100000; // The order of these is important; we want the WaitAny call to be signaled @@ -1271,17 +1306,11 @@ private bool TryGetConnection(DbConnection owningObject, uint waitForMultipleObj if (onlyOneCheckConnection) { - if (_waitHandles.CreationSemaphore.WaitOne(unchecked((int)waitForMultipleObjectsTimeout))) + using SemaphoreHolder semaphoreHolder = new(_waitHandles.CreationSemaphore, unchecked((int)waitForMultipleObjectsTimeout)); + if (semaphoreHolder.Obtained) { - try - { - SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Creating new connection.", Id); - obj = UserCreateRequest(owningObject, userOptions); - } - finally - { - _waitHandles.CreationSemaphore.Release(1); - } + SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, Creating new connection.", Id); + obj = UserCreateRequest(owningObject, userOptions); } else { @@ -1500,16 +1529,13 @@ private void PoolCreateRequest(object state) { return; } - int waitResult = BOGUS_HANDLE; - + try { // Obtain creation mutex so we're the only one creating objects - // and we must have the wait result - bool creationHandleObtained = _waitHandles.CreationSemaphore.WaitOne(CreationTimeout); - waitResult = creationHandleObtained ? CREATION_HANDLE : WaitHandle.WaitTimeout; - - if (CREATION_HANDLE == waitResult) + using SemaphoreHolder semaphoreHolder = new(_waitHandles.CreationSemaphore, CreationTimeout); + + if (semaphoreHolder.Obtained) { DbConnectionInternal newObj; @@ -1563,14 +1589,6 @@ private void PoolCreateRequest(object state) // thrown to the user the next time they request a connection. SqlClientEventSource.Log.TryPoolerTraceEvent(" {0}, PoolCreateRequest called CreateConnection which threw an exception: {1}", Id, e); } - finally - { - if (CREATION_HANDLE == waitResult) - { - // reuse waitResult and ignore its value - _waitHandles.CreationSemaphore.Release(1); - } - } } } } From 48f797f272942603c721e6e0f740dc6dbc08823b Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 1 Oct 2025 21:15:25 -0700 Subject: [PATCH 3/7] Add tests --- src/Microsoft.Data.SqlClient.sln | 93 ++++ ....Data.SqlClient.ManualTesting.Tests.csproj | 1 + .../ConnectionPoolStressTest.cs | 433 ++++++++++++++++++ 3 files changed, 527 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index e4d29d999c..78fc88199f 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -303,6 +303,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Data.SqlClient.Un EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Microsoft.Data.SqlClient\tests\Common\Common.csproj", "{67128EC0-30F5-6A98-448B-55F88A1DE707}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "connpoolbenchmark", "..\..\connpoolbenchmark\connpoolbenchmark\connpoolbenchmark.csproj", "{35C1A807-372A-47C5-854A-3DABF505BB55}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Stress", "Stress", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IMonitorLoader", "Microsoft.Data.SqlClient\tests\StressTests\IMonitorLoader\IMonitorLoader.csproj", "{1A29B520-D16A-35F2-5CAC-64573C86E63D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Common", "Microsoft.Data.SqlClient\tests\StressTests\SqlClient.Stress.Common\SqlClient.Stress.Common.csproj", "{2AA12D54-540B-E515-CB82-80D691C9DCF1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Framework", "Microsoft.Data.SqlClient\tests\StressTests\SqlClient.Stress.Framework\SqlClient.Stress.Framework.csproj", "{92D9C6D6-6925-1AD1-69FA-485F83943BD2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Runner", "Microsoft.Data.SqlClient\tests\StressTests\SqlClient.Stress.Runner\SqlClient.Stress.Runner.csproj", "{4A9C11F4-9577-ABEC-C070-83A194746D9B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlClient.Stress.Tests", "Microsoft.Data.SqlClient\tests\StressTests\SqlClient.Stress.Tests\SqlClient.Stress.Tests.csproj", "{FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -571,6 +585,78 @@ Global {67128EC0-30F5-6A98-448B-55F88A1DE707}.Release|x64.Build.0 = Release|x64 {67128EC0-30F5-6A98-448B-55F88A1DE707}.Release|x86.ActiveCfg = Release|x86 {67128EC0-30F5-6A98-448B-55F88A1DE707}.Release|x86.Build.0 = Release|x86 + {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|x64.ActiveCfg = Debug|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|x64.Build.0 = Debug|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|x86.ActiveCfg = Debug|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|x86.Build.0 = Debug|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|Any CPU.Build.0 = Release|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|x64.ActiveCfg = Release|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|x64.Build.0 = Release|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|x86.ActiveCfg = Release|Any CPU + {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|x86.Build.0 = Release|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Debug|x64.Build.0 = Debug|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Debug|x86.Build.0 = Debug|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Release|Any CPU.Build.0 = Release|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Release|x64.ActiveCfg = Release|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Release|x64.Build.0 = Release|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Release|x86.ActiveCfg = Release|Any CPU + {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Release|x86.Build.0 = Release|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Debug|x64.ActiveCfg = Debug|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Debug|x64.Build.0 = Debug|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Debug|x86.ActiveCfg = Debug|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Debug|x86.Build.0 = Debug|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Release|Any CPU.Build.0 = Release|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Release|x64.ActiveCfg = Release|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Release|x64.Build.0 = Release|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Release|x86.ActiveCfg = Release|Any CPU + {2AA12D54-540B-E515-CB82-80D691C9DCF1}.Release|x86.Build.0 = Release|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Debug|x64.ActiveCfg = Debug|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Debug|x64.Build.0 = Debug|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Debug|x86.ActiveCfg = Debug|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Debug|x86.Build.0 = Debug|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Release|Any CPU.Build.0 = Release|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Release|x64.ActiveCfg = Release|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Release|x64.Build.0 = Release|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Release|x86.ActiveCfg = Release|Any CPU + {92D9C6D6-6925-1AD1-69FA-485F83943BD2}.Release|x86.Build.0 = Release|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Debug|x64.ActiveCfg = Debug|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Debug|x64.Build.0 = Debug|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Debug|x86.ActiveCfg = Debug|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Debug|x86.Build.0 = Debug|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Release|Any CPU.Build.0 = Release|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Release|x64.ActiveCfg = Release|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Release|x64.Build.0 = Release|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Release|x86.ActiveCfg = Release|Any CPU + {4A9C11F4-9577-ABEC-C070-83A194746D9B}.Release|x86.Build.0 = Release|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Debug|x64.ActiveCfg = Debug|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Debug|x64.Build.0 = Debug|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Debug|x86.ActiveCfg = Debug|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Debug|x86.Build.0 = Debug|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Release|Any CPU.Build.0 = Release|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Release|x64.ActiveCfg = Release|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Release|x64.Build.0 = Release|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Release|x86.ActiveCfg = Release|Any CPU + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -621,6 +707,13 @@ Global {AD738BD4-6A02-4B88-8F93-FBBBA49A74C8} = {4CAE9195-4F1A-4D48-854C-1C9FBC512C66} {4461063D-2F2B-274C-7E6F-F235119D258E} = {0CC4817A-12F3-4357-912C-09315FAAD008} {67128EC0-30F5-6A98-448B-55F88A1DE707} = {0CC4817A-12F3-4357-912C-09315FAAD008} + {35C1A807-372A-47C5-854A-3DABF505BB55} = {0CC4817A-12F3-4357-912C-09315FAAD008} + {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {0CC4817A-12F3-4357-912C-09315FAAD008} + {1A29B520-D16A-35F2-5CAC-64573C86E63D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {2AA12D54-540B-E515-CB82-80D691C9DCF1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {92D9C6D6-6925-1AD1-69FA-485F83943BD2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {4A9C11F4-9577-ABEC-C070-83A194746D9B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {FAA1E517-581A-D3DC-BAC9-FAD1D5A5142C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {01D48116-37A2-4D33-B9EC-94793C702431} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 94819c9bb4..220c2c48af 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -257,6 +257,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs new file mode 100644 index 0000000000..c8fbb95ab7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs @@ -0,0 +1,433 @@ +// 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.Generic; +using System.Data.Common; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + /// + /// Connection pool stress test to validate pool behavior under various concurrent load scenarios. + /// + public class ConnectionPoolStressTest + { + #region Properties + + /// + /// Connection string + /// + internal string ConnectionString { get; set; } + + /// + /// Maximum number of connections in the pool + /// + public int MaxPoolSize { get; set; } = 100; + + /// + /// SQL WAITFOR DELAY value for simulating slow queries + /// + public string WaitForDelay { get; set; } = "00:00:00.100"; + + /// + /// Number of concurrent connections to create + /// + public int ConcurrentConnections { get; set; } = 10; + + /// + /// Number of operations each thread should perform + /// + public int OperationsPerThread { get; set; } = 10; + + #endregion + + #region Connection Dooming + + // Reflection fields for accessing internal connection properties + private static readonly FieldInfo _msDataInternalConnectionField; + + static ConnectionPoolStressTest() + { + try + { + // Cache reflection info for Microsoft.Data.SqlClient + Type msDataConnectionType = typeof(SqlConnection); + _msDataInternalConnectionField = msDataConnectionType.GetField("_innerConnection", BindingFlags.NonPublic | BindingFlags.Instance); + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Failed to initialize reflection for connection dooming: {ex.Message}"); + } + } + + #region Tests + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + [TestCategory("LongRunning")] // Takes around 13 seconds. + public async Task ConnectionPoolStress_Sync() + { + var test = new ConnectionPoolStressTest + { + MaxPoolSize = 100, + ConcurrentConnections = 10, + WaitForDelay = "00:00:00.100", + OperationsPerThread = 100, + }; + + test.SetConnectionString(DataTestUtility.TCPConnectionString); + + // Run the stress tests + if (!RunSingleStressTest(test.ConnectionPoolStress_MsData_Sync)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); + } + + if(!await TestConnectionPoolExhaustion(test, false)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + [TestCategory("LongRunning")]// Takes around 11 seconds. + public async Task ConnectionPoolStress_Async() + { + var test = new ConnectionPoolStressTest + { + MaxPoolSize = 100, + ConcurrentConnections = 10, + WaitForDelay = "00:00:00.100", + OperationsPerThread = 100, + }; + + test.SetConnectionString(DataTestUtility.TCPConnectionString); + + // Test Microsoft.Data.SqlClient Async + if (!RunSingleStressTest(test.ConnectionPoolStress_MsData_Async)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Async failed"); + } + + // Test connection pool exhaustion + if(!await TestConnectionPoolExhaustion(test, true)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Async failed"); + } + } + + #endregion + + #region Helpers + + private static bool RunSingleStressTest(Action testAction) + { + try + { + var stopwatch = Stopwatch.StartNew(); + testAction(); + stopwatch.Stop(); + } + catch (Exception ex) + { + if (ex.InnerException != null) + { + return false; + } + } + + return true; + } + + private static async Task TestConnectionPoolExhaustion(ConnectionPoolStressTest test, bool async) + { + // Test Microsoft.Data.SqlClient + return await TestConnectionPoolExhaustionForProvider(test.ConnectionString, test.MaxPoolSize, "Microsoft.Data.SqlClient", async, + cs => new SqlConnection(cs)); + } + + private static async Task TestConnectionPoolExhaustionForProvider(string connectionString, int maxPoolSize, string providerName, bool async, Func connectionFactory) + { + var connections = new List(); + + try + { + for (int i = 0; i < maxPoolSize; i++) + { + var conn = connectionFactory(connectionString); + if (async) + { + await conn.OpenAsync(); + } + else + conn.Open(); + connections.Add(conn); + } + + } + catch + { + return false; + } + finally + { + // Clean up all connections + foreach (var conn in connections) + { + conn?.Dispose(); + } + } + + return true; + } + + #endregion + + /// + /// Dooms a Microsoft.Data.SqlClient connection by calling DoomThisConnection on its internal connection + /// + private static bool DoomMicrosoftDataConnection(SqlConnection connection) + { + try + { + if (_msDataInternalConnectionField?.GetValue(connection) is object internalConnection) + { + var doomMethod = internalConnection.GetType().GetMethod("DoomThisConnection", BindingFlags.NonPublic | BindingFlags.Instance); + if (doomMethod != null) + { + doomMethod.Invoke(internalConnection, null); + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + catch (Exception) + { + return false; + } + } + + #endregion + + #region Configuration + + /// + /// Sets up connection strings for both SQL client libraries with optimized settings + /// + /// Connection string to be set. + internal void SetConnectionString(string connectionString) + { + var connectionSB = new SqlConnectionStringBuilder(connectionString) + { + // Min size needs to be larger than the number of concurrent connections to trigger the pool exhaustion as it will make it more likely that PoolCreateRequest will run. + MinPoolSize = Math.Min(20, MaxPoolSize / 5), // Dynamic min pool size + MaxPoolSize = MaxPoolSize, + Pooling = true, // Explicitly enable pooling + TrustServerCertificate = true + }; + + ConnectionString = connectionSB.ConnectionString; + + // Ensure adequate thread pool capacity + ThreadPool.SetMaxThreads(Math.Max(ConcurrentConnections * 2, 100), 100); + } + + #endregion + + #region Stress Test Methods + + /// + /// Runs a synchronous stress test using Microsoft.Data.SqlClient with connection dooming + /// + internal void ConnectionPoolStress_MsData_Sync() + { + RunStressTest( + connectionString: ConnectionString, + doomAction: conn => DoomMicrosoftDataConnection((SqlConnection)conn), + async: false + ); + } + + /// + /// Runs asynchronous stress test using Microsoft.Data.SqlClient with connection dooming + /// + internal void ConnectionPoolStress_MsData_Async() + { + RunStressTest( + connectionString: ConnectionString, + doomAction: conn => DoomMicrosoftDataConnection((SqlConnection)conn), + async: true + ); + } + + /// + /// Generic stress test method that works with both SQL client libraries using DbConnection/DbCommand + /// + private void RunStressTest( + string connectionString, + Func doomAction, + bool async = false) + { + var threads = new Thread[ConcurrentConnections]; + var barrier = new Barrier(ConcurrentConnections); + var countdown = new CountdownEvent(ConcurrentConnections); + + var command = string.IsNullOrWhiteSpace(WaitForDelay) + ? "SELECT GETDATE()" + : $"WAITFOR DELAY '{WaitForDelay}'; SELECT GETDATE()"; + + // Create regular threads (don't doom connections) + for (int i = 0; i < ConcurrentConnections - 1; i++) + { + threads[i] = CreateWorkerThread( + connectionString, command, barrier, countdown, doomConnections: false, async); + } + + // Create special thread that dooms connections (if we have multiple threads) + if (ConcurrentConnections > 1) + { + threads[ConcurrentConnections - 1] = CreateWorkerThread( + connectionString, command, barrier, countdown, doomConnections: true, async, doomAction); + } + + // Start all threads + foreach (var thread in threads.Where(t => t != null)) + { + thread.Start(); + } + + // Wait for completion + countdown.Wait(); + + // Clean up synchronization objects + barrier?.Dispose(); + countdown?.Dispose(); + } + + /// + /// Creates a worker thread that performs database operations using DbConnection/DbCommand + /// + private Thread CreateWorkerThread( + string connectionString, + string command, + Barrier barrier, + CountdownEvent countdown, + bool doomConnections, + bool async, + Func doomAction = null) + { + return new Thread(async () => + { + try + { + barrier.SignalAndWait(); // Initial synchronization - all threads start together + + for (int j = 0; j < OperationsPerThread; j++) + { + if (doomConnections && doomAction != null) + { + // Dooming thread - barriers inside using block to doom before disposal + using (var conn = new SqlConnection(connectionString)) + { + if (async) + { + await conn.OpenAsync(); + } + else + conn.Open(); + + await ExecuteCommand(command, async, conn); + + // Synchronize after command execution, before dooming + barrier.SignalAndWait(); + + // Doom connection before it gets disposed/returned to pool + if (!doomAction(conn)) + { + // fail test here. + } + + // Synchronize after dooming - ensures all threads see the effect + barrier.SignalAndWait(); + } + } + else + { + // Non-dooming threads - barriers after connection is closed + using (var conn = new SqlConnection(connectionString)) + { + if (async) + { + await conn.OpenAsync(); + } + else + conn.Open(); + + await ExecuteCommand(command, async, conn); + + } // Connection is closed/returned to pool here + + // Synchronize after connection is closed + barrier.SignalAndWait(); + + // Sync for coordination with dooming thread + barrier.SignalAndWait(); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Thread execution failed: {ex.Message}"); + } + finally + { + countdown.Signal(); + } + }) + { + IsBackground = true // Make threads background threads for cleaner shutdown + }; + } + + /// + /// Executes a database command with proper error handling + /// + private static async Task ExecuteCommand(string command, bool async, SqlConnection conn) + { + try + { + using (var cmd = new SqlCommand(command, conn)) + { + if (async) + { + await cmd.ExecuteScalarAsync(); + } + else + cmd.ExecuteScalar(); + } + } + catch (Exception ex) + { + Console.WriteLine($"Command execution failed: {ex.Message}"); + } + } + + #endregion + } +} From baf7d3d8843b41bd22ab5705ecca85f90d375497 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 1 Oct 2025 21:35:26 -0700 Subject: [PATCH 4/7] Update sln --- src/Microsoft.Data.SqlClient.sln | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index 78fc88199f..94900021ac 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -303,8 +303,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Data.SqlClient.Un EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Microsoft.Data.SqlClient\tests\Common\Common.csproj", "{67128EC0-30F5-6A98-448B-55F88A1DE707}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "connpoolbenchmark", "..\..\connpoolbenchmark\connpoolbenchmark\connpoolbenchmark.csproj", "{35C1A807-372A-47C5-854A-3DABF505BB55}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Stress", "Stress", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IMonitorLoader", "Microsoft.Data.SqlClient\tests\StressTests\IMonitorLoader\IMonitorLoader.csproj", "{1A29B520-D16A-35F2-5CAC-64573C86E63D}" @@ -585,18 +583,6 @@ Global {67128EC0-30F5-6A98-448B-55F88A1DE707}.Release|x64.Build.0 = Release|x64 {67128EC0-30F5-6A98-448B-55F88A1DE707}.Release|x86.ActiveCfg = Release|x86 {67128EC0-30F5-6A98-448B-55F88A1DE707}.Release|x86.Build.0 = Release|x86 - {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|Any CPU.Build.0 = Debug|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|x64.ActiveCfg = Debug|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|x64.Build.0 = Debug|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|x86.ActiveCfg = Debug|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Debug|x86.Build.0 = Debug|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|Any CPU.ActiveCfg = Release|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|Any CPU.Build.0 = Release|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|x64.ActiveCfg = Release|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|x64.Build.0 = Release|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|x86.ActiveCfg = Release|Any CPU - {35C1A807-372A-47C5-854A-3DABF505BB55}.Release|x86.Build.0 = Release|Any CPU {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A29B520-D16A-35F2-5CAC-64573C86E63D}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -707,7 +693,6 @@ Global {AD738BD4-6A02-4B88-8F93-FBBBA49A74C8} = {4CAE9195-4F1A-4D48-854C-1C9FBC512C66} {4461063D-2F2B-274C-7E6F-F235119D258E} = {0CC4817A-12F3-4357-912C-09315FAAD008} {67128EC0-30F5-6A98-448B-55F88A1DE707} = {0CC4817A-12F3-4357-912C-09315FAAD008} - {35C1A807-372A-47C5-854A-3DABF505BB55} = {0CC4817A-12F3-4357-912C-09315FAAD008} {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {0CC4817A-12F3-4357-912C-09315FAAD008} {1A29B520-D16A-35F2-5CAC-64573C86E63D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {2AA12D54-540B-E515-CB82-80D691C9DCF1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} From e2cc20f74e3055f252152c8091ff955495518484 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 1 Oct 2025 21:43:02 -0700 Subject: [PATCH 5/7] Test formatting --- .../ConnectionPoolStressTest.cs | 256 +++++++++--------- 1 file changed, 128 insertions(+), 128 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs index c8fbb95ab7..703ce1b8e4 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs @@ -51,7 +51,7 @@ public class ConnectionPoolStressTest #region Connection Dooming // Reflection fields for accessing internal connection properties - private static readonly FieldInfo _msDataInternalConnectionField; + private static readonly FieldInfo _internalConnectionField; static ConnectionPoolStressTest() { @@ -59,7 +59,7 @@ static ConnectionPoolStressTest() { // Cache reflection info for Microsoft.Data.SqlClient Type msDataConnectionType = typeof(SqlConnection); - _msDataInternalConnectionField = msDataConnectionType.GetField("_innerConnection", BindingFlags.NonPublic | BindingFlags.Instance); + _internalConnectionField = msDataConnectionType.GetField("_innerConnection", BindingFlags.NonPublic | BindingFlags.Instance); } catch (Exception ex) { @@ -67,131 +67,6 @@ static ConnectionPoolStressTest() } } - #region Tests - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - [TestCategory("LongRunning")] // Takes around 13 seconds. - public async Task ConnectionPoolStress_Sync() - { - var test = new ConnectionPoolStressTest - { - MaxPoolSize = 100, - ConcurrentConnections = 10, - WaitForDelay = "00:00:00.100", - OperationsPerThread = 100, - }; - - test.SetConnectionString(DataTestUtility.TCPConnectionString); - - // Run the stress tests - if (!RunSingleStressTest(test.ConnectionPoolStress_MsData_Sync)) - { - // fail the test - Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); - } - - if(!await TestConnectionPoolExhaustion(test, false)) - { - // fail the test - Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); - } - } - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - [TestCategory("LongRunning")]// Takes around 11 seconds. - public async Task ConnectionPoolStress_Async() - { - var test = new ConnectionPoolStressTest - { - MaxPoolSize = 100, - ConcurrentConnections = 10, - WaitForDelay = "00:00:00.100", - OperationsPerThread = 100, - }; - - test.SetConnectionString(DataTestUtility.TCPConnectionString); - - // Test Microsoft.Data.SqlClient Async - if (!RunSingleStressTest(test.ConnectionPoolStress_MsData_Async)) - { - // fail the test - Assert.Fail("ConnectionPoolStress_MsData_Async failed"); - } - - // Test connection pool exhaustion - if(!await TestConnectionPoolExhaustion(test, true)) - { - // fail the test - Assert.Fail("ConnectionPoolStress_MsData_Async failed"); - } - } - - #endregion - - #region Helpers - - private static bool RunSingleStressTest(Action testAction) - { - try - { - var stopwatch = Stopwatch.StartNew(); - testAction(); - stopwatch.Stop(); - } - catch (Exception ex) - { - if (ex.InnerException != null) - { - return false; - } - } - - return true; - } - - private static async Task TestConnectionPoolExhaustion(ConnectionPoolStressTest test, bool async) - { - // Test Microsoft.Data.SqlClient - return await TestConnectionPoolExhaustionForProvider(test.ConnectionString, test.MaxPoolSize, "Microsoft.Data.SqlClient", async, - cs => new SqlConnection(cs)); - } - - private static async Task TestConnectionPoolExhaustionForProvider(string connectionString, int maxPoolSize, string providerName, bool async, Func connectionFactory) - { - var connections = new List(); - - try - { - for (int i = 0; i < maxPoolSize; i++) - { - var conn = connectionFactory(connectionString); - if (async) - { - await conn.OpenAsync(); - } - else - conn.Open(); - connections.Add(conn); - } - - } - catch - { - return false; - } - finally - { - // Clean up all connections - foreach (var conn in connections) - { - conn?.Dispose(); - } - } - - return true; - } - - #endregion - /// /// Dooms a Microsoft.Data.SqlClient connection by calling DoomThisConnection on its internal connection /// @@ -199,7 +74,7 @@ private static bool DoomMicrosoftDataConnection(SqlConnection connection) { try { - if (_msDataInternalConnectionField?.GetValue(connection) is object internalConnection) + if (_internalConnectionField?.GetValue(connection) is object internalConnection) { var doomMethod = internalConnection.GetType().GetMethod("DoomThisConnection", BindingFlags.NonPublic | BindingFlags.Instance); if (doomMethod != null) @@ -429,5 +304,130 @@ private static async Task ExecuteCommand(string command, bool async, SqlConnecti } #endregion + + #region Helpers + + private static bool RunSingleStressTest(Action testAction) + { + try + { + var stopwatch = Stopwatch.StartNew(); + testAction(); + stopwatch.Stop(); + } + catch (Exception ex) + { + if (ex.InnerException != null) + { + return false; + } + } + + return true; + } + + private static async Task TestConnectionPoolExhaustion(ConnectionPoolStressTest test, bool async) + { + // Test Microsoft.Data.SqlClient + return await TestConnectionPoolExhaustionForProvider(test.ConnectionString, test.MaxPoolSize, "Microsoft.Data.SqlClient", async, + cs => new SqlConnection(cs)); + } + + private static async Task TestConnectionPoolExhaustionForProvider(string connectionString, int maxPoolSize, string providerName, bool async, Func connectionFactory) + { + var connections = new List(); + + try + { + for (int i = 0; i < maxPoolSize; i++) + { + var conn = connectionFactory(connectionString); + if (async) + { + await conn.OpenAsync(); + } + else + conn.Open(); + connections.Add(conn); + } + + } + catch + { + return false; + } + finally + { + // Clean up all connections + foreach (var conn in connections) + { + conn?.Dispose(); + } + } + + return true; + } + + #endregion + + #region Pool Exhaustion Tests + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + [TestCategory("LongRunning")] // Takes around 13 seconds. + public async Task ConnectionPoolStress_Sync() + { + var test = new ConnectionPoolStressTest + { + MaxPoolSize = 100, + ConcurrentConnections = 10, + WaitForDelay = "00:00:00.100", + OperationsPerThread = 100, + }; + + test.SetConnectionString(DataTestUtility.TCPConnectionString); + + // Run the stress tests + if (!RunSingleStressTest(test.ConnectionPoolStress_MsData_Sync)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); + } + + if(!await TestConnectionPoolExhaustion(test, false)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + [TestCategory("LongRunning")] // Takes around 11 seconds. + public async Task ConnectionPoolStress_Async() + { + var test = new ConnectionPoolStressTest + { + MaxPoolSize = 100, + ConcurrentConnections = 10, + WaitForDelay = "00:00:00.100", + OperationsPerThread = 100, + }; + + test.SetConnectionString(DataTestUtility.TCPConnectionString); + + // Test Microsoft.Data.SqlClient Async + if (!RunSingleStressTest(test.ConnectionPoolStress_MsData_Async)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Async failed"); + } + + // Test connection pool exhaustion + if(!await TestConnectionPoolExhaustion(test, true)) + { + // fail the test + Assert.Fail("ConnectionPoolStress_MsData_Async failed"); + } + } + + #endregion } } From 6eaa4a0994430016defbe21f1a6392bda89c3455 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 2 Oct 2025 10:52:01 -0700 Subject: [PATCH 6/7] Minor changes --- .../ConnectionPoolStressTest.cs | 130 ++++++++---------- 1 file changed, 60 insertions(+), 70 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs index 703ce1b8e4..776859ecf6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs @@ -76,7 +76,7 @@ private static bool DoomMicrosoftDataConnection(SqlConnection connection) { if (_internalConnectionField?.GetValue(connection) is object internalConnection) { - var doomMethod = internalConnection.GetType().GetMethod("DoomThisConnection", BindingFlags.NonPublic | BindingFlags.Instance); + MethodInfo doomMethod = internalConnection.GetType().GetMethod("DoomThisConnection", BindingFlags.NonPublic | BindingFlags.Instance); if (doomMethod != null) { doomMethod.Invoke(internalConnection, null); @@ -163,36 +163,41 @@ private void RunStressTest( var barrier = new Barrier(ConcurrentConnections); var countdown = new CountdownEvent(ConcurrentConnections); - var command = string.IsNullOrWhiteSpace(WaitForDelay) - ? "SELECT GETDATE()" - : $"WAITFOR DELAY '{WaitForDelay}'; SELECT GETDATE()"; - - // Create regular threads (don't doom connections) - for (int i = 0; i < ConcurrentConnections - 1; i++) + try { - threads[i] = CreateWorkerThread( - connectionString, command, barrier, countdown, doomConnections: false, async); - } + var command = string.IsNullOrWhiteSpace(WaitForDelay) + ? "SELECT GETDATE()" + : $"WAITFOR DELAY '{WaitForDelay}'; SELECT GETDATE()"; - // Create special thread that dooms connections (if we have multiple threads) - if (ConcurrentConnections > 1) - { - threads[ConcurrentConnections - 1] = CreateWorkerThread( - connectionString, command, barrier, countdown, doomConnections: true, async, doomAction); - } + // Create regular threads (don't doom connections) + for (int i = 0; i < ConcurrentConnections - 1; i++) + { + threads[i] = CreateWorkerThread( + connectionString, command, barrier, countdown, doomConnections: false, async); + } - // Start all threads - foreach (var thread in threads.Where(t => t != null)) - { - thread.Start(); - } + // Create special thread that dooms connections (if we have multiple threads) + if (ConcurrentConnections > 1) + { + threads[ConcurrentConnections - 1] = CreateWorkerThread( + connectionString, command, barrier, countdown, doomConnections: true, async, doomAction); + } - // Wait for completion - countdown.Wait(); + // Start all threads + foreach (Thread thread in threads.Where(t => t != null)) + { + thread.Start(); + } - // Clean up synchronization objects - barrier?.Dispose(); - countdown?.Dispose(); + // Wait for completion + countdown.Wait(); + } + finally + { + // Clean up synchronization objects + barrier?.Dispose(); + countdown?.Dispose(); + } } /// @@ -218,29 +223,27 @@ private Thread CreateWorkerThread( if (doomConnections && doomAction != null) { // Dooming thread - barriers inside using block to doom before disposal - using (var conn = new SqlConnection(connectionString)) + using var conn = new SqlConnection(connectionString); + if (async) { - if (async) - { - await conn.OpenAsync(); - } - else - conn.Open(); - - await ExecuteCommand(command, async, conn); + await conn.OpenAsync(); + } + else + conn.Open(); - // Synchronize after command execution, before dooming - barrier.SignalAndWait(); + await ExecuteCommand(command, async, conn); - // Doom connection before it gets disposed/returned to pool - if (!doomAction(conn)) - { - // fail test here. - } + // Synchronize after command execution, before dooming + barrier.SignalAndWait(); - // Synchronize after dooming - ensures all threads see the effect - barrier.SignalAndWait(); + // Doom connection before it gets disposed/returned to pool + if (!doomAction(conn)) + { + throw new Exception("Unable to doom connection"); } + + // Synchronize after dooming - ensures all threads see the effect + barrier.SignalAndWait(); } else { @@ -266,10 +269,6 @@ private Thread CreateWorkerThread( } } } - catch (Exception ex) - { - Console.WriteLine($"Thread execution failed: {ex.Message}"); - } finally { countdown.Signal(); @@ -287,15 +286,13 @@ private static async Task ExecuteCommand(string command, bool async, SqlConnecti { try { - using (var cmd = new SqlCommand(command, conn)) + using var cmd = new SqlCommand(command, conn); + if (async) { - if (async) - { - await cmd.ExecuteScalarAsync(); - } - else - cmd.ExecuteScalar(); + await cmd.ExecuteScalarAsync(); } + else + cmd.ExecuteScalar(); } catch (Exception ex) { @@ -326,22 +323,15 @@ private static bool RunSingleStressTest(Action testAction) return true; } - private static async Task TestConnectionPoolExhaustion(ConnectionPoolStressTest test, bool async) + private static async Task TestConnectionPoolExhaustion(string connectionString, int maxPoolSize, bool async) { - // Test Microsoft.Data.SqlClient - return await TestConnectionPoolExhaustionForProvider(test.ConnectionString, test.MaxPoolSize, "Microsoft.Data.SqlClient", async, - cs => new SqlConnection(cs)); - } - - private static async Task TestConnectionPoolExhaustionForProvider(string connectionString, int maxPoolSize, string providerName, bool async, Func connectionFactory) - { - var connections = new List(); + var connections = new List(); try { for (int i = 0; i < maxPoolSize; i++) { - var conn = connectionFactory(connectionString); + SqlConnection conn = new(connectionString); if (async) { await conn.OpenAsync(); @@ -350,7 +340,7 @@ private static async Task TestConnectionPoolExhaustionForProvider(string c conn.Open(); connections.Add(conn); } - + Assert.Equal(maxPoolSize, connections.Count); } catch { @@ -359,7 +349,7 @@ private static async Task TestConnectionPoolExhaustionForProvider(string c finally { // Clean up all connections - foreach (var conn in connections) + foreach (SqlConnection conn in connections) { conn?.Dispose(); } @@ -392,7 +382,7 @@ public async Task ConnectionPoolStress_Sync() Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); } - if(!await TestConnectionPoolExhaustion(test, false)) + if(!await TestConnectionPoolExhaustion(test.ConnectionString, test.MaxPoolSize, false)) { // fail the test Assert.Fail("ConnectionPoolStress_MsData_Sync failed"); @@ -420,8 +410,8 @@ public async Task ConnectionPoolStress_Async() Assert.Fail("ConnectionPoolStress_MsData_Async failed"); } - // Test connection pool exhaustion - if(!await TestConnectionPoolExhaustion(test, true)) + // Test connection pool exhaustion (async) + if(!await TestConnectionPoolExhaustion(test.ConnectionString, test.MaxPoolSize, true)) { // fail the test Assert.Fail("ConnectionPoolStress_MsData_Async failed"); From d74a9fed0834ba5591e3b18fcd07c8a90ba91d0a Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 2 Oct 2025 16:30:14 -0700 Subject: [PATCH 7/7] use using --- .../ConnectionPoolStressTest.cs | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs index 776859ecf6..c8d4705d2d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs @@ -160,44 +160,35 @@ private void RunStressTest( bool async = false) { var threads = new Thread[ConcurrentConnections]; - var barrier = new Barrier(ConcurrentConnections); - var countdown = new CountdownEvent(ConcurrentConnections); + using Barrier barrier = new(ConcurrentConnections); + using CountdownEvent countdown = new(ConcurrentConnections); - try - { - var command = string.IsNullOrWhiteSpace(WaitForDelay) - ? "SELECT GETDATE()" - : $"WAITFOR DELAY '{WaitForDelay}'; SELECT GETDATE()"; - - // Create regular threads (don't doom connections) - for (int i = 0; i < ConcurrentConnections - 1; i++) - { - threads[i] = CreateWorkerThread( - connectionString, command, barrier, countdown, doomConnections: false, async); - } - - // Create special thread that dooms connections (if we have multiple threads) - if (ConcurrentConnections > 1) - { - threads[ConcurrentConnections - 1] = CreateWorkerThread( - connectionString, command, barrier, countdown, doomConnections: true, async, doomAction); - } + var command = string.IsNullOrWhiteSpace(WaitForDelay) + ? "SELECT GETDATE()" + : $"WAITFOR DELAY '{WaitForDelay}'; SELECT GETDATE()"; - // Start all threads - foreach (Thread thread in threads.Where(t => t != null)) - { - thread.Start(); - } + // Create regular threads (don't doom connections) + for (int i = 0; i < ConcurrentConnections - 1; i++) + { + threads[i] = CreateWorkerThread( + connectionString, command, barrier, countdown, doomConnections: false, async); + } - // Wait for completion - countdown.Wait(); + // Create special thread that dooms connections (if we have multiple threads) + if (ConcurrentConnections > 1) + { + threads[ConcurrentConnections - 1] = CreateWorkerThread( + connectionString, command, barrier, countdown, doomConnections: true, async, doomAction); } - finally + + // Start all threads + foreach (Thread thread in threads.Where(t => t != null)) { - // Clean up synchronization objects - barrier?.Dispose(); - countdown?.Dispose(); + thread.Start(); } + + // Wait for completion + countdown.Wait(); } ///