diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln
index e4d29d999c..94900021ac 100644
--- a/src/Microsoft.Data.SqlClient.sln
+++ b/src/Microsoft.Data.SqlClient.sln
@@ -303,6 +303,18 @@ 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("{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 +583,66 @@ 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
+ {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 +693,12 @@ 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}
+ {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/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/WaitHandleDbConnectionPool.cs
index 4cef28fd78..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,14 +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
- waitResult = WaitHandle.WaitAny(_waitHandles.GetHandles(withCreate: true), CreationTimeout);
- if (CREATION_HANDLE == waitResult)
+ using SemaphoreHolder semaphoreHolder = new(_waitHandles.CreationSemaphore, CreationTimeout);
+
+ if (semaphoreHolder.Obtained)
{
DbConnectionInternal newObj;
@@ -1542,17 +1570,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)
{
@@ -1566,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);
- }
- }
}
}
}
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..d3d41d4c9a
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionPoolTest/ConnectionPoolStressTest.cs
@@ -0,0 +1,440 @@
+// 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;
+
+#nullable enable
+
+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 readonly FieldInfo? _internalConnectionField;
+
+ public ConnectionPoolStressTest()
+ {
+ try
+ {
+ // Cache reflection info for Microsoft.Data.SqlClient
+ Type msDataConnectionType = typeof(SqlConnection);
+ _internalConnectionField = msDataConnectionType.GetField("_innerConnection", BindingFlags.NonPublic | BindingFlags.Instance);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Warning: Failed to initialize reflection for connection dooming: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Dooms a Microsoft.Data.SqlClient connection by calling DoomThisConnection on its internal connection
+ ///
+ private bool DoomMicrosoftDataConnection(SqlConnection connection)
+ {
+ try
+ {
+ if(_internalConnectionField == null)
+ {
+ // Fail the test if reflection setup failed
+ return false;
+ }
+
+ if (_internalConnectionField.GetValue(connection) is object internalConnection)
+ {
+ MethodInfo? 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 string
+ ///
+ /// 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()
+ {
+ if (ConnectionString == null)
+ {
+ throw new InvalidOperationException("ConnectionString is not set. Call SetConnectionString() before running the test.");
+ }
+
+ 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()
+ {
+ if (ConnectionString == null)
+ {
+ throw new InvalidOperationException("ConnectionString is not set. Call SetConnectionString() before running the test.");
+ }
+
+ 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];
+ using Barrier barrier = new(ConcurrentConnections);
+ using CountdownEvent countdown = new(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 (Thread thread in threads.Where(t => t != null))
+ {
+ thread.Start();
+ }
+
+ // Wait for completion
+ countdown.Wait();
+ }
+
+ ///
+ /// 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))
+ {
+ throw new Exception("Unable to doom connection");
+ }
+
+ // 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();
+ }
+ }
+ }
+ 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
+
+ #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(string connectionString, int maxPoolSize, bool async)
+ {
+ var connections = new List();
+
+ try
+ {
+ for (int i = 0; i < maxPoolSize; i++)
+ {
+ SqlConnection conn = new(connectionString);
+ if (async)
+ {
+ await conn.OpenAsync();
+ }
+ else
+ {
+ conn.Open();
+ }
+ connections.Add(conn);
+ }
+ Assert.Equal(maxPoolSize, connections.Count);
+ }
+ catch
+ {
+ return false;
+ }
+ finally
+ {
+ // Clean up all connections
+ foreach (SqlConnection 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.ConnectionString!, test.MaxPoolSize, 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 (async)
+ if (!await TestConnectionPoolExhaustion(test.ConnectionString!, test.MaxPoolSize, true))
+ {
+ // fail the test
+ Assert.Fail("ConnectionPoolStress_MsData_Async failed");
+ }
+ }
+
+ #endregion
+ }
+}