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 + } +}