From d543a1b805f19855d166c36a1c361598bf09e5c6 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 6 Oct 2025 12:47:31 -0500 Subject: [PATCH 01/15] Reenable tests in SqlCommandCancelTest: TimeoutCancelTcp, TimeoutCancelNamedPipe, TimeoutDuringReadTcp, TimeoutDuringReadNamedPipe --- .../SQL/SqlCommand/SqlCommandCancelTest.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs index 0928a71569..7717bc39c9 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs @@ -152,15 +152,14 @@ public static void MultiThreadedCancel_AsyncNP() // Synapse: WAITFOR not supported + ';' not supported. [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] - public static void TimeoutCancel() + public static void TimeoutCancelTcp() { TimeoutCancel(tcp_connStr); } - [ActiveIssue("12167")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] [PlatformSpecific(TestPlatforms.Windows)] - public static void TimeoutCancelNP() + public static void TimeoutCancelNamedPipe() { TimeoutCancel(np_connStr); } @@ -178,17 +177,15 @@ public static void CancelAndDisposePreparedCommandNP() CancelAndDisposePreparedCommand(np_connStr); } - [ActiveIssue("5541")] - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - public static void TimeOutDuringRead() + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotNamedInstance))] + public static void TimeOutDuringReadTcp() { TimeOutDuringRead(tcp_connStr); } - [ActiveIssue("5541")] - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.IsNotNamedInstance))] [PlatformSpecific(TestPlatforms.Windows)] - public static void TimeOutDuringReadNP() + public static void TimeOutDuringReadNamedPipe() { TimeOutDuringRead(np_connStr); } From cafa83bfd3abb3272d17dd0c826b7ef81a3f2982 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 6 Oct 2025 13:23:01 -0500 Subject: [PATCH 02/15] Reenable tests in SqlCredentialTest - SqlConnectionChangePasswordPlaintext, SqlConnectionChangePasswordSecureString --- .../ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs index ae656f11a4..5c345922c7 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs @@ -44,7 +44,6 @@ public static void CreateSqlConnectionWithCredential() } } - [ActiveIssue("9196")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] public static void SqlConnectionChangePasswordPlaintext() { @@ -78,7 +77,6 @@ public static void SqlConnectionChangePasswordPlaintext() } } - [ActiveIssue("9196")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] public static void SqlConnectionChangePasswordSecureString() { From b5dd2fe1ff75fbfe67530ae2a55f3b455536d6c2 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 6 Oct 2025 14:31:19 -0500 Subject: [PATCH 03/15] Very light cleanup of SqlCredentialTest --- .../SqlCredentialTest/SqlCredentialTest.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs index 5c345922c7..dd3132a41c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCredentialTest/SqlCredentialTest.cs @@ -16,11 +16,11 @@ public static class SqlCredentialTest public static void CreateSqlConnectionWithCredential() { var user = "u" + Guid.NewGuid().ToString().Replace("-", ""); - var passStr = "Pax561O$T5K#jD"; + const string passStr = "Pax561O$T5K#jD"; try { - createTestUser(user, passStr); + CreateTestUser(user, passStr); var csb = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString); csb.Remove("User ID"); @@ -40,7 +40,7 @@ public static void CreateSqlConnectionWithCredential() } finally { - dropTestUser(user); + DropTestUser(user); } } @@ -48,12 +48,12 @@ public static void CreateSqlConnectionWithCredential() public static void SqlConnectionChangePasswordPlaintext() { var user = "u" + Guid.NewGuid().ToString().Replace("-", ""); - var pass = "!21Ja3Ims7LI&n"; - var newPass = "fmVCNf@24Dg*8j"; + const string pass = "!21Ja3Ims7LI&n"; + const string newPass = "fmVCNf@24Dg*8j"; try { - createTestUser(user, pass); + CreateTestUser(user, pass); var csb = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString); csb.UserID = user; @@ -73,7 +73,7 @@ public static void SqlConnectionChangePasswordPlaintext() } finally { - dropTestUser(user); + DropTestUser(user); } } @@ -81,12 +81,12 @@ public static void SqlConnectionChangePasswordPlaintext() public static void SqlConnectionChangePasswordSecureString() { var user = "u" + Guid.NewGuid().ToString().Replace("-", ""); - var passStr = "tcM0qB^izt%3u7"; - var newPassStr = "JSG2e(Vp0WCXE&"; + const string passStr = "tcM0qB^izt%3u7"; + const string newPassStr = "JSG2e(Vp0WCXE&"; try { - createTestUser(user, passStr); + CreateTestUser(user, passStr); var csb = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString); csb.Remove("User ID"); @@ -113,19 +113,19 @@ public static void SqlConnectionChangePasswordSecureString() } finally { - dropTestUser(user); + DropTestUser(user); } } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] public static void OldCredentialsShouldFail() { - String user = "u" + Guid.NewGuid().ToString().Replace("-", ""); - String passStr = "Pax561O$T5K#jD"; + string user = "u" + Guid.NewGuid().ToString().Replace("-", ""); + const string passStr = "Pax561O$T5K#jD"; try { - createTestUser(user, passStr); + CreateTestUser(user, passStr); SqlConnectionStringBuilder sqlConnectionStringBuilder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString); sqlConnectionStringBuilder.Remove("User ID"); @@ -176,11 +176,11 @@ public static void OldCredentialsShouldFail() } finally { - dropTestUser(user); + DropTestUser(user); } } - private static void createTestUser(string username, string password) + private static void CreateTestUser(string username, string password) { // Creates a test user with read permissions. string createUserCmd = $"CREATE LOGIN {username} WITH PASSWORD = '{password}', CHECK_POLICY=OFF;" @@ -194,7 +194,7 @@ private static void createTestUser(string username, string password) } } - private static void dropTestUser(string username) + private static void DropTestUser(string username) { // Removes a created test user. string dropUserCmd = $"IF EXISTS (SELECT * FROM sys.schemas WHERE name = '{username}') BEGIN DROP SCHEMA {username} END;" From 0815d29a60301019031ae4572bede796007cb922 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 6 Oct 2025 14:45:30 -0500 Subject: [PATCH 04/15] Reenable MarsSessionPoolingTest in MarsExecuteReader_Text_WithGC and MarsExecuteReader_StoredProcedure_WithGC # Conflicts: # src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs --- .../SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs index c32bbeb668..7893fc5417 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs @@ -67,7 +67,6 @@ public static void MarsExecuteReader_RPC_NoGC() } // Synapse: Catalog view 'dm_exec_connections' is not supported in this version. - [ActiveIssue("11167")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] public static void MarsExecuteReader_Text_WithGC() { @@ -76,7 +75,6 @@ public static void MarsExecuteReader_Text_WithGC() } // Synapse: Stored procedure sp_who does not exist or is not supported. - [ActiveIssue("8959")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] public static void MarsExecuteReader_StoredProcedure_WithGC() { From 317f456df52629bcd204a3bdf0959444f6b68f97 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 7 Oct 2025 13:00:13 -0500 Subject: [PATCH 05/15] Split tests into separate tests for each scenario, discover which tests are flaky --- .../ManualTests/DataCommon/DataTestUtility.cs | 3 + .../MARSSessionPoolingTest.cs | 204 ++++++++++++++---- 2 files changed, 161 insertions(+), 46 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 916b9ac04d..ff9a704b9a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -409,6 +409,9 @@ public static bool AreConnStringsSetup() return !string.IsNullOrEmpty(NPConnectionString) && !string.IsNullOrEmpty(TCPConnectionString); } + public static bool AreConnStringsNotAzureSynapse() => + AreConnStringsSetup() && IsNotAzureSynapse(); + public static bool IsSQL2022() => string.Equals("16", SQLServerVersion.Trim()); public static bool IsSQL2019() => string.Equals("15", SQLServerVersion.Trim()); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs index 7893fc5417..896f393358 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs @@ -9,7 +9,7 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests { - public static class MARSSessionPoolingTest + public class MARSSessionPoolingTest { private const string COMMAND_STATUS = "select count(*) as ConnectionCount, @@spid as spid from sys.dm_exec_connections where session_id=@@spid and net_transport='Session'; " + "select count(*) as ActiveRequestCount, @@spid as spid from sys.dm_exec_requests where session_id=@@spid and status='running' or session_id=@@spid and status='suspended'"; @@ -33,57 +33,169 @@ public static class MARSSessionPoolingTest }).ConnectionString; // Synapse: Catalog view 'dm_exec_connections' is not supported in this version. - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public static void MarsExecuteScalar_AllFlavors() - { - TestMARSSessionPooling("Case: Text, ExecuteScalar", _testConnString, CommandType.Text, ExecuteType.ExecuteScalar, ReaderTestType.ReaderClose, GCType.Wait); - TestMARSSessionPooling("Case: RPC, ExecuteScalar", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteScalar, ReaderTestType.ReaderClose, GCType.Wait); - } - // Synapse: Catalog view 'dm_exec_connections' is not supported in this version. - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public static void MarsExecuteNonQuery_AllFlavors() - { - TestMARSSessionPooling("Case: Text, ExecuteNonQuery", _testConnString, CommandType.Text, ExecuteType.ExecuteNonQuery, ReaderTestType.ReaderClose, GCType.Wait); - TestMARSSessionPooling("Case: RPC, ExecuteNonQuery", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteNonQuery, ReaderTestType.ReaderClose, GCType.Wait); - } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteScalar_Text() => + TestMARSSessionPooling( + "Case: Text, ExecuteScalar", + _testConnString, + CommandType.Text, + ExecuteType.ExecuteScalar, + ReaderTestType.ReaderClose, + GCType.Wait); - // Synapse: Catalog view 'dm_exec_connections' is not supported in this version. - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public static void MarsExecuteReader_Text_NoGC() - { - TestMARSSessionPooling("Case: Text, ExecuteReader, ReaderClose", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ReaderClose, GCType.Wait); - TestMARSSessionPooling("Case: Text, ExecuteReader, ReaderDispose", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ReaderDispose, GCType.Wait); - TestMARSSessionPooling("Case: Text, ExecuteReader, ConnectionClose", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ConnectionClose, GCType.Wait); - } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteScalar_Sproc() => + TestMARSSessionPooling( + "Case: RPC, ExecuteScalar", + _testConnString, + CommandType.StoredProcedure, + ExecuteType.ExecuteScalar, + ReaderTestType.ReaderClose, + GCType.Wait); - // Synapse: Stored procedure sp_who does not exist or is not supported. - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public static void MarsExecuteReader_RPC_NoGC() - { - TestMARSSessionPooling("Case: RPC, ExecuteReader, ReaderClose", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ReaderClose, GCType.Wait); - TestMARSSessionPooling("Case: RPC, ExecuteReader, ReaderDispose", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ReaderDispose, GCType.Wait); - TestMARSSessionPooling("Case: RPC, ExecuteReader, ConnectionClose", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ConnectionClose, GCType.Wait); - } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteNonQuery_Text() => + TestMARSSessionPooling( + "Case: Text, ExecuteNonQuery", + _testConnString, + CommandType.Text, + ExecuteType.ExecuteNonQuery, + ReaderTestType.ReaderClose, + GCType.Wait); - // Synapse: Catalog view 'dm_exec_connections' is not supported in this version. - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public static void MarsExecuteReader_Text_WithGC() - { - TestMARSSessionPooling("Case: Text, ExecuteReader, GC-Wait", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ReaderGC, GCType.Wait); - TestMARSSessionPooling("Case: Text, ExecuteReader, GC-NoWait", _testConnString, CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.ReaderGC, GCType.NoWait); - } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteNonQuery_Sproc() => + TestMARSSessionPooling( + "Case: RPC, ExecuteNonQuery", + _testConnString, + CommandType.StoredProcedure, + ExecuteType.ExecuteNonQuery, + ReaderTestType.ReaderClose, + GCType.Wait); - // Synapse: Stored procedure sp_who does not exist or is not supported. - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public static void MarsExecuteReader_StoredProcedure_WithGC() - { - TestMARSSessionPooling("Case: RPC, ExecuteReader, GC-Wait", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ReaderGC, GCType.Wait); - TestMARSSessionPooling("Case: RPC, ExecuteReader, GC-NoWait", _testConnString, CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.ReaderGC, GCType.NoWait); + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_TextNoGc_ReaderClose() => + TestMARSSessionPooling( + "Case: Text, ExecuteReader, ReaderClose", + _testConnString, + CommandType.Text, + ExecuteType.ExecuteReader, + ReaderTestType.ReaderClose, + GCType.Wait); - TestMARSSessionPooling("Case: Text, ExecuteReader, NoCloses", _testConnString + " ", CommandType.Text, ExecuteType.ExecuteReader, ReaderTestType.NoCloses, GCType.Wait); - TestMARSSessionPooling("Case: RPC, ExecuteReader, NoCloses", _testConnString + " ", CommandType.StoredProcedure, ExecuteType.ExecuteReader, ReaderTestType.NoCloses, GCType.Wait); - } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_TextNoGc_ReaderDispose() => + TestMARSSessionPooling( + "Case: Text, ExecuteReader, ReaderDispose", + _testConnString, + CommandType.Text, + ExecuteType.ExecuteReader, + ReaderTestType.ReaderDispose, + GCType.Wait); + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_TextNoGc_ConnectionClose() => + TestMARSSessionPooling( + "Case: Text, ExecuteReader, ConnectionClose", + _testConnString, + CommandType.Text, + ExecuteType.ExecuteReader, + ReaderTestType.ConnectionClose, + GCType.Wait); + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_SprocNoGc_ReaderClose() => + TestMARSSessionPooling( + "Case: RPC, ExecuteReader, ReaderClose", + _testConnString, + CommandType.StoredProcedure, + ExecuteType.ExecuteReader, + ReaderTestType.ReaderClose, + GCType.Wait); + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_SprocNoGc_ReaderDispose() => + TestMARSSessionPooling( + "Case: RPC, ExecuteReader, ReaderDispose", + _testConnString, + CommandType.StoredProcedure, + ExecuteType.ExecuteReader, + ReaderTestType.ReaderDispose, + GCType.Wait); + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_SprocNoGc_ConnectionClose() => + TestMARSSessionPooling( + "Case: RPC, ExecuteReader, ConnectionClose", + _testConnString, + CommandType.StoredProcedure, + ExecuteType.ExecuteReader, + ReaderTestType.ConnectionClose, + GCType.Wait); + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_TextWithGc_GcWait() => + TestMARSSessionPooling( + "Case: Text, ExecuteReader, GC-Wait", + _testConnString, + CommandType.Text, + ExecuteType.ExecuteReader, + ReaderTestType.ReaderGC, + GCType.Wait); + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_TextWithGc_GcNoWait() => + TestMARSSessionPooling( + "Case: Text, ExecuteReader, GC-NoWait", + _testConnString, + CommandType.Text, + ExecuteType.ExecuteReader, + ReaderTestType.ReaderGC, + GCType.NoWait); + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_SprocWithGc_GcWait() => + TestMARSSessionPooling( + "Case: RPC, ExecuteReader, GC-Wait", + _testConnString, + CommandType.StoredProcedure, + ExecuteType.ExecuteReader, + ReaderTestType.ReaderGC, + GCType.Wait); + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_SprocWithGc_GcNoWait() => + TestMARSSessionPooling( + "Case: RPC, ExecuteReader, GC-Wait", + _testConnString, + CommandType.StoredProcedure, + ExecuteType.ExecuteReader, + ReaderTestType.ReaderGC, + GCType.Wait); + + // Disabling as this there is a race condition somewhere in it + [ActiveIssue("Flaky, race condition")] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_TextWithGc_NoCloses() => + TestMARSSessionPooling( + "Case: Text, ExecuteReader, NoCloses", + _testConnString + " ", + CommandType.Text, + ExecuteType.ExecuteReader, + ReaderTestType.NoCloses, + GCType.Wait); + + [ActiveIssue("Flaky, race condition")] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + public void MarsExecuteReader_SprocWithGc_NoCloses() => + TestMARSSessionPooling( + "Case: RPC, ExecuteReader, NoCloses", + _testConnString + " ", + CommandType.StoredProcedure, + ExecuteType.ExecuteReader, + ReaderTestType.NoCloses, + GCType.Wait); private enum ExecuteType { From 20f46f739ca22faeed19324a35acec2b1eee6db1 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 8 Oct 2025 11:26:35 -0500 Subject: [PATCH 06/15] Mostly working . Except for those no close tests --- .../tests/Common/DisposableArray.cs | 48 ++ .../MARSSessionPoolingTest.cs | 615 +++++++++++------- 2 files changed, 430 insertions(+), 233 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs diff --git a/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs b/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs new file mode 100644 index 0000000000..bb4ec95e1c --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs @@ -0,0 +1,48 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.Data.SqlClient.Tests.Common +{ + public class DisposableArray : IDisposable, IEnumerable + where T : IDisposable + { + private readonly T[] _elements; + + public T this[int i] + { + get => _elements[i]; + set => _elements[i] = value; + } + + public int Length => _elements.Length; + + public DisposableArray(int size) + { + _elements = new T[size]; + } + + public DisposableArray(T[] elements) + { + _elements = elements; + } + + public void Dispose() + { + foreach (T element in _elements) + { + element?.Dispose(); + } + } + + public IEnumerator GetEnumerator() => + ((IEnumerable)_elements).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + _elements.GetEnumerator(); + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs index 896f393358..30495b1278 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs @@ -5,14 +5,16 @@ using System; using System.Data; using System.Runtime.CompilerServices; +using Microsoft.Data.SqlClient.Tests.Common; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { public class MARSSessionPoolingTest { - private const string COMMAND_STATUS = "select count(*) as ConnectionCount, @@spid as spid from sys.dm_exec_connections where session_id=@@spid and net_transport='Session'; " + - "select count(*) as ActiveRequestCount, @@spid as spid from sys.dm_exec_requests where session_id=@@spid and status='running' or session_id=@@spid and status='suspended'"; + private const string COMMAND_STATUS = + "select count(*) as ConnectionCount, @@spid as spid from sys.dm_exec_connections where session_id=@@spid and net_transport='Session'; " + + "select count(*) as ActiveRequestCount, @@spid as spid from sys.dm_exec_requests where session_id=@@spid and (status='running' or status='suspended')"; private const string COMMAND_SPID = "select @@spid"; private const int CONCURRENT_COMMANDS = 5; @@ -34,168 +36,316 @@ public class MARSSessionPoolingTest // Synapse: Catalog view 'dm_exec_connections' is not supported in this version. - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteScalar_Text() => - TestMARSSessionPooling( - "Case: Text, ExecuteScalar", - _testConnString, - CommandType.Text, - ExecuteType.ExecuteScalar, - ReaderTestType.ReaderClose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteScalar_Sproc() => - TestMARSSessionPooling( - "Case: RPC, ExecuteScalar", - _testConnString, - CommandType.StoredProcedure, - ExecuteType.ExecuteScalar, - ReaderTestType.ReaderClose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteNonQuery_Text() => - TestMARSSessionPooling( - "Case: Text, ExecuteNonQuery", - _testConnString, - CommandType.Text, - ExecuteType.ExecuteNonQuery, - ReaderTestType.ReaderClose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteNonQuery_Sproc() => - TestMARSSessionPooling( - "Case: RPC, ExecuteNonQuery", - _testConnString, - CommandType.StoredProcedure, - ExecuteType.ExecuteNonQuery, - ReaderTestType.ReaderClose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_TextNoGc_ReaderClose() => - TestMARSSessionPooling( - "Case: Text, ExecuteReader, ReaderClose", - _testConnString, - CommandType.Text, - ExecuteType.ExecuteReader, - ReaderTestType.ReaderClose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_TextNoGc_ReaderDispose() => - TestMARSSessionPooling( - "Case: Text, ExecuteReader, ReaderDispose", - _testConnString, - CommandType.Text, - ExecuteType.ExecuteReader, - ReaderTestType.ReaderDispose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_TextNoGc_ConnectionClose() => - TestMARSSessionPooling( - "Case: Text, ExecuteReader, ConnectionClose", - _testConnString, - CommandType.Text, - ExecuteType.ExecuteReader, - ReaderTestType.ConnectionClose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_SprocNoGc_ReaderClose() => - TestMARSSessionPooling( - "Case: RPC, ExecuteReader, ReaderClose", - _testConnString, - CommandType.StoredProcedure, - ExecuteType.ExecuteReader, - ReaderTestType.ReaderClose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_SprocNoGc_ReaderDispose() => - TestMARSSessionPooling( - "Case: RPC, ExecuteReader, ReaderDispose", - _testConnString, - CommandType.StoredProcedure, - ExecuteType.ExecuteReader, - ReaderTestType.ReaderDispose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_SprocNoGc_ConnectionClose() => - TestMARSSessionPooling( - "Case: RPC, ExecuteReader, ConnectionClose", - _testConnString, - CommandType.StoredProcedure, - ExecuteType.ExecuteReader, - ReaderTestType.ConnectionClose, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_TextWithGc_GcWait() => - TestMARSSessionPooling( - "Case: Text, ExecuteReader, GC-Wait", - _testConnString, - CommandType.Text, - ExecuteType.ExecuteReader, - ReaderTestType.ReaderGC, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_TextWithGc_GcNoWait() => - TestMARSSessionPooling( - "Case: Text, ExecuteReader, GC-NoWait", - _testConnString, - CommandType.Text, - ExecuteType.ExecuteReader, - ReaderTestType.ReaderGC, - GCType.NoWait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_SprocWithGc_GcWait() => - TestMARSSessionPooling( - "Case: RPC, ExecuteReader, GC-Wait", - _testConnString, - CommandType.StoredProcedure, - ExecuteType.ExecuteReader, - ReaderTestType.ReaderGC, - GCType.Wait); - - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_SprocWithGc_GcNoWait() => - TestMARSSessionPooling( - "Case: RPC, ExecuteReader, GC-Wait", - _testConnString, - CommandType.StoredProcedure, - ExecuteType.ExecuteReader, - ReaderTestType.ReaderGC, - GCType.Wait); - - // Disabling as this there is a race condition somewhere in it - [ActiveIssue("Flaky, race condition")] - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_TextWithGc_NoCloses() => - TestMARSSessionPooling( - "Case: Text, ExecuteReader, NoCloses", - _testConnString + " ", - CommandType.Text, - ExecuteType.ExecuteReader, - ReaderTestType.NoCloses, - GCType.Wait); - - [ActiveIssue("Flaky, race condition")] - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] - public void MarsExecuteReader_SprocWithGc_NoCloses() => - TestMARSSessionPooling( - "Case: RPC, ExecuteReader, NoCloses", - _testConnString + " ", - CommandType.StoredProcedure, - ExecuteType.ExecuteReader, - ReaderTestType.NoCloses, - GCType.Wait); + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteScalar(CommandType commandType) + { + // Arrange + using SqlConnection connection = new SqlConnection(_testConnString); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // Run command, close/reopen connection to dispose sessions + command.ExecuteScalar(); + connection.Close(); + connection.Open(); + + // Assert + AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteNonQuery(CommandType commandType) + { + // Arrange + using SqlConnection connection = new SqlConnection(_testConnString); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // Run command, close/reopen connection to dispose sessions + command.ExecuteScalar(); + connection.Close(); + connection.Open(); + + // Assert + AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_CloseReader(CommandType commandType) + { + // Arrange + using SqlConnection connection = new SqlConnection(_testConnString); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // Run command, close reader + using SqlDataReader reader = command.ExecuteReader(); + reader.Close(); + + // Close/reopen connection to force disposal of sessions + connection.Close(); + connection.Open(); + + // Assert + AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_DisposeReader(CommandType commandType) + { + // Arrange + using SqlConnection connection = new SqlConnection(_testConnString); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // Run command, dispose reader + SqlDataReader reader = command.ExecuteReader(); + reader.Dispose(); + + // Close/reopen connection to force disposal of sessions + connection.Close(); + connection.Open(); + + // Assert + AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_CloseConnection(CommandType commandType) + { + // Arrange + using SqlConnection connection = new SqlConnection(_testConnString); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // Run command, suppress finalization of reader + using SqlDataReader reader = command.ExecuteReader(); + GC.SuppressFinalize(reader); + + // Close/reopen connection to force disposal of sessions + connection.Close(); + connection.Open(); + + // Assert + AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_GarbageCollection_Wait(CommandType commandType) + { + // Arrange + using SqlConnection connection = new SqlConnection(_testConnString); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // Run command and get weak reference to reader + // Note: This must happen in another scope otherwise the reader will not be marked + // for garbage collection + WeakReference weakReader = OpenReaderThenNullify(command); + + // Run the garbage collector to force cleanup of reader + GC.Collect(); + GC.WaitForPendingFinalizers(); + + // Close/reopen connection to force disposal of sessions + connection.Close(); + connection.Open(); + + // Assert + // Reader should be garbage collected by now + Assert.False(weakReader.IsAlive); + AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_GarbageCollection_NoWait(CommandType commandType) + { + // Arrange + using SqlConnection connection = new SqlConnection(_testConnString); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // Run command + // Note: This must happen in another scope otherwise the reader will not be marked + // for garbage collection + _ = OpenReaderThenNullify(command); + + // Run the garbage collector, but do not wait for finalization + GC.Collect(); + + // Close/reopen connection to force disposal of sessions + connection.Close(); + connection.Open(); + + // Assert + AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_NoCloses(CommandType commandType) + { + // Arrange + using SqlConnection connection = new SqlConnection(_testConnString); + using DisposableArray commands = GetCommands(connection, commandType); + using DisposableArray readers = new DisposableArray(commands.Length); + connection.Open(); + + // Act / Assert + for (int i = 0; i < commands.Length; i++) + { + using SqlDataReader reader = commands[i].ExecuteReader(); + GC.SuppressFinalize(reader); + + // // Act + // // Run command, close nothing! + // readers[i] = commands[i].ExecuteReader(); + // GC.SuppressFinalize(readers[i]); + // + // // Assert + // // MARS session for all previous commands should still be open + // // Sessions: 1 for connection, i+1 for previous commands (with 0-index offset) + // // Requests: i+1 for previous commands (with 0-index offset) + // AssertSessionsAndRequests(connection, expectedSessions: i + 2, expectedRequests: i + 1); + AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); + } + + // foreach (var q in readers) + // { + // q.Close(); + // q.Dispose(); + // } + // + // foreach (var q in commands) + // { + // q.Dispose(); + // } + // + // connection.Close(); + // connection.Open(); + // connection.Dispose(); + } + + private DisposableArray GetCommands(SqlConnection connection, CommandType commandType) + { + SqlCommand[] result = new SqlCommand[CONCURRENT_COMMANDS]; + for (int i = 0; i < result.Length; i++) + { + switch (commandType) + { + case CommandType.Text: + result[i] = new SqlCommand + { + CommandText = + "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + + "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + + "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + + "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + + "select * from sys.databases; print 'THIS IS THE END!'", + CommandTimeout = 120, + CommandType = CommandType.Text, + Connection = connection + }; + break; + + case CommandType.StoredProcedure: + result[i] = new SqlCommand + { + CommandText = "sp_who", + CommandTimeout = 120, + CommandType = CommandType.StoredProcedure, + Connection = connection + }; + break; + + default: + throw new InvalidOperationException("Not supported test type"); + } + } + + return new DisposableArray(result); + } + + private void AssertSessionsAndRequests(SqlConnection connection, int expectedSessions, int expectedRequests) + { + using SqlCommand verificationCommand = new SqlCommand(); + verificationCommand.CommandText = + "select count(*) as MarsSessionCount from sys.dm_exec_connections where session_id=@@spid and net_transport='Session'; " + + "select count(*) as ActiveRequestCount from sys.dm_exec_requests where session_id=@@spid and (status='running' or status='suspended')"; + verificationCommand.CommandType = CommandType.Text; + verificationCommand.Connection = connection; + + using SqlDataReader reader = verificationCommand.ExecuteReader(); + + // Result 1) Count of active MARS sessions from sys.dm_exec_connections + if (!reader.Read()) + { + throw new Exception("Expected dm_exec_connections results from verification command"); + } + + // Add 1 for the verification command executing + Assert.Equal(expectedSessions + 1, reader.GetInt32(0)); + + // Result 2) Count of active requests from sys.dm_exec_requests + if (!reader.NextResult() || !reader.Read()) + { + throw new Exception("Expected dm_exec_requests results from verification command"); + } + + // Add 1 for the verification command executing + Assert.Equal(expectedRequests + 1, reader.GetInt32(0)); + } + private enum ExecuteType { @@ -230,6 +380,7 @@ private static void TestMARSSessionPooling(string caseName, string connectionStr { con.Open(); + // Create command for (int i = 0; i < CONCURRENT_COMMANDS; i++) { // Prepare all commands @@ -252,12 +403,12 @@ private static void TestMARSSessionPooling(string caseName, string connectionStr { switch (executeType) { - case ExecuteType.ExecuteScalar: - cmd[i].ExecuteScalar(); - break; - case ExecuteType.ExecuteNonQuery: - cmd[i].ExecuteNonQuery(); - break; + // case ExecuteType.ExecuteScalar: + // cmd[i].ExecuteScalar(); + // break; + // case ExecuteType.ExecuteNonQuery: + // cmd[i].ExecuteNonQuery(); + // break; case ExecuteType.ExecuteReader: if (readerTestType != ReaderTestType.ReaderGC) { @@ -266,30 +417,30 @@ private static void TestMARSSessionPooling(string caseName, string connectionStr switch (readerTestType) { - case ReaderTestType.ReaderClose: - { - gch[i].Dispose(); - break; - } - case ReaderTestType.ReaderDispose: - gch[i].Dispose(); - break; - case ReaderTestType.ReaderGC: - gch[i] = null; - WeakReference weak = OpenReaderThenNullify(cmd[i]); - GC.Collect(); - - if (gcType == GCType.Wait) - { - GC.WaitForPendingFinalizers(); - Assert.False(weak.IsAlive, "Error - target still alive!"); - } - break; - case ReaderTestType.ConnectionClose: - GC.SuppressFinalize(gch[i]); - con.Close(); - con.Open(); - break; + // case ReaderTestType.ReaderClose: + // { + // gch[i].Dispose(); + // break; + // } + // case ReaderTestType.ReaderDispose: + // gch[i].Dispose(); + // break; + // case ReaderTestType.ReaderGC: + // // gch[i] = null; + // // WeakReference weak = OpenReaderThenNullify(cmd[i]); + // // GC.Collect(); + // + // // if (gcType == GCType.Wait) + // // { + // // GC.WaitForPendingFinalizers(); + // // Assert.False(weak.IsAlive, "Error - target still alive!"); + // // } + // break; + // case ReaderTestType.ConnectionClose: + // GC.SuppressFinalize(gch[i]); + // con.Close(); + // con.Open(); + // break; case ReaderTestType.NoCloses: GC.SuppressFinalize(gch[i]); break; @@ -319,52 +470,50 @@ private static void TestMARSSessionPooling(string caseName, string connectionStr switch (executeType) { - case ExecuteType.ExecuteScalar: - case ExecuteType.ExecuteNonQuery: - // 1 for connection, 1 for command - Assert.True(connections == 2, "Failure - incorrect number of connections for ExecuteScalar! #connections: " + connections); - - // only 1 executing - Assert.True(requests == 1, "Failure - incorrect number of requests for ExecuteScalar! #requests: " + requests); - break; + // case ExecuteType.ExecuteScalar: + // case ExecuteType.ExecuteNonQuery: + // // 1 for connection, 1 for command + // Assert.True(connections == 2, "Failure - incorrect number of connections for ExecuteScalar! #connections: " + connections); + // + // // only 1 executing + // Assert.True(requests == 1, "Failure - incorrect number of requests for ExecuteScalar! #requests: " + requests); + // break; case ExecuteType.ExecuteReader: switch (readerTestType) { - case ReaderTestType.ReaderClose: - case ReaderTestType.ReaderDispose: - case ReaderTestType.ConnectionClose: - // 1 for connection, 1 for command - Assert.True(connections == 2, "Failure - Incorrect number of connections for ReaderClose / ReaderDispose / ConnectionClose! #connections: " + connections); - - // only 1 executing - Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderClose/ReaderDispose/ConnectionClose! #requests: " + requests); - break; - case ReaderTestType.ReaderGC: - switch (gcType) - { - case GCType.Wait: - // 1 for connection, 1 for open reader - Assert.True(connections == 2, "Failure - incorrect number of connections for ReaderGCWait! #connections: " + connections); - // only 1 executing - Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderGCWait! #requests: " + requests); - break; - case GCType.NoWait: - // 1 for connection, 1 for open reader - Assert.True(connections == 2, "Failure - incorrect number of connections for ReaderGCNoWait! #connections: " + connections); - - // only 1 executing - Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderGCNoWait! #requests: " + requests); - break; - } - break; + // case ReaderTestType.ReaderClose: + // case ReaderTestType.ReaderDispose: + // case ReaderTestType.ConnectionClose: + // // 1 for connection, 1 for command + // Assert.True(connections == 2, "Failure - Incorrect number of connections for ReaderClose / ReaderDispose / ConnectionClose! #connections: " + connections); + // + // // only 1 executing + // Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderClose/ReaderDispose/ConnectionClose! #requests: " + requests); + // break; + // case ReaderTestType.ReaderGC: + // switch (gcType) + // { + // // case GCType.Wait: + // // // 1 for connection, 1 for open reader + // // Assert.True(connections == 2, "Failure - incorrect number of connections for ReaderGCWait! #connections: " + connections); + // // // only 1 executing + // // Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderGCWait! #requests: " + requests); + // // break; + // case GCType.NoWait: + // // 1 for connection, 1 for open reader + // Assert.True(connections == 2, "Failure - incorrect number of connections for ReaderGCNoWait! #connections: " + connections); + // + // // only 1 executing + // Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderGCNoWait! #requests: " + requests); + // break; + // } + // break; case ReaderTestType.NoCloses: // 1 for connection, 1 for current command, 1 for 0 based array offset, plus i for open readers - Assert.True(connections == (3 + i), "Failure - incorrect number of connections for NoCloses: " + connections + - "\ni: " + i + " :::: requests: " + requests + " :::: spid1: " + spid1 + " ::::: spid2: " + spid2); + Assert.Equal(3+i, connections); // 1 for current command, 1 for 0 based array offset, plus i open readers - Assert.True(requests == (2 + i), "Failure - incorrect number of requests for NoCloses: " + requests + - "\ni: " + i + " :::: connections: " + connections + " :::: spid1: " + spid1 + " ::::: spid2: " + spid2); + Assert.Equal(2+i, requests); break; } break; From c1f9ac16d276314d6422c6a30fca26d93a3821e5 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 9 Oct 2025 17:45:41 -0500 Subject: [PATCH 07/15] Clean up old code, but the nocloses test still breaks everything - need to investigate if it's a bug --- .../MARSSessionPoolingTest.cs | 297 ++---------------- 1 file changed, 33 insertions(+), 264 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs index 30495b1278..4c58a1ddf2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs @@ -4,6 +4,7 @@ using System; using System.Data; +using System.Linq; using System.Runtime.CompilerServices; using Microsoft.Data.SqlClient.Tests.Common; using Xunit; @@ -12,27 +13,16 @@ namespace Microsoft.Data.SqlClient.ManualTesting.Tests { public class MARSSessionPoolingTest { - private const string COMMAND_STATUS = - "select count(*) as ConnectionCount, @@spid as spid from sys.dm_exec_connections where session_id=@@spid and net_transport='Session'; " + - "select count(*) as ActiveRequestCount, @@spid as spid from sys.dm_exec_requests where session_id=@@spid and (status='running' or status='suspended')"; - private const string COMMAND_SPID = "select @@spid"; - private const int CONCURRENT_COMMANDS = 5; - - private const string _COMMAND_RPC = "sp_who"; - private const string _COMMAND_SQL = - "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + - "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + - "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + - "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + - "select * from sys.databases; print 'THIS IS THE END!'"; - - private static readonly string _testConnString = - (new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) + private const int ConcurrentCommands = 5; + private static readonly string TestConnString = + new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { + ApplicationName = "SqlClientMarsPoolingTests", // Specify application name to make these tests unique + // for pooling purposes. PacketSize = 512, MaxPoolSize = 1, MultipleActiveResultSets = true - }).ConnectionString; + }.ConnectionString; // Synapse: Catalog view 'dm_exec_connections' is not supported in this version. @@ -42,7 +32,7 @@ public class MARSSessionPoolingTest public void ExecuteScalar(CommandType commandType) { // Arrange - using SqlConnection connection = new SqlConnection(_testConnString); + using SqlConnection connection = new SqlConnection(TestConnString); using DisposableArray commands = GetCommands(connection, commandType); connection.Open(); @@ -50,10 +40,8 @@ public void ExecuteScalar(CommandType commandType) foreach (SqlCommand command in commands) { // Act - // Run command, close/reopen connection to dispose sessions + // Run command command.ExecuteScalar(); - connection.Close(); - connection.Open(); // Assert AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); @@ -66,7 +54,7 @@ public void ExecuteScalar(CommandType commandType) public void ExecuteNonQuery(CommandType commandType) { // Arrange - using SqlConnection connection = new SqlConnection(_testConnString); + using SqlConnection connection = new SqlConnection(TestConnString); using DisposableArray commands = GetCommands(connection, commandType); connection.Open(); @@ -74,10 +62,8 @@ public void ExecuteNonQuery(CommandType commandType) foreach (SqlCommand command in commands) { // Act - // Run command, close/reopen connection to dispose sessions + // Run command command.ExecuteScalar(); - connection.Close(); - connection.Open(); // Assert AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); @@ -90,7 +76,7 @@ public void ExecuteNonQuery(CommandType commandType) public void ExecuteReader_CloseReader(CommandType commandType) { // Arrange - using SqlConnection connection = new SqlConnection(_testConnString); + using SqlConnection connection = new SqlConnection(TestConnString); using DisposableArray commands = GetCommands(connection, commandType); connection.Open(); @@ -102,10 +88,6 @@ public void ExecuteReader_CloseReader(CommandType commandType) using SqlDataReader reader = command.ExecuteReader(); reader.Close(); - // Close/reopen connection to force disposal of sessions - connection.Close(); - connection.Open(); - // Assert AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); } @@ -117,7 +99,7 @@ public void ExecuteReader_CloseReader(CommandType commandType) public void ExecuteReader_DisposeReader(CommandType commandType) { // Arrange - using SqlConnection connection = new SqlConnection(_testConnString); + using SqlConnection connection = new SqlConnection(TestConnString); using DisposableArray commands = GetCommands(connection, commandType); connection.Open(); @@ -129,10 +111,6 @@ public void ExecuteReader_DisposeReader(CommandType commandType) SqlDataReader reader = command.ExecuteReader(); reader.Dispose(); - // Close/reopen connection to force disposal of sessions - connection.Close(); - connection.Open(); - // Assert AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); } @@ -144,7 +122,7 @@ public void ExecuteReader_DisposeReader(CommandType commandType) public void ExecuteReader_CloseConnection(CommandType commandType) { // Arrange - using SqlConnection connection = new SqlConnection(_testConnString); + using SqlConnection connection = new SqlConnection(TestConnString); using DisposableArray commands = GetCommands(connection, commandType); connection.Open(); @@ -156,10 +134,6 @@ public void ExecuteReader_CloseConnection(CommandType commandType) using SqlDataReader reader = command.ExecuteReader(); GC.SuppressFinalize(reader); - // Close/reopen connection to force disposal of sessions - connection.Close(); - connection.Open(); - // Assert AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); } @@ -171,7 +145,7 @@ public void ExecuteReader_CloseConnection(CommandType commandType) public void ExecuteReader_GarbageCollection_Wait(CommandType commandType) { // Arrange - using SqlConnection connection = new SqlConnection(_testConnString); + using SqlConnection connection = new SqlConnection(TestConnString); using DisposableArray commands = GetCommands(connection, commandType); connection.Open(); @@ -188,10 +162,6 @@ public void ExecuteReader_GarbageCollection_Wait(CommandType commandType) GC.Collect(); GC.WaitForPendingFinalizers(); - // Close/reopen connection to force disposal of sessions - connection.Close(); - connection.Open(); - // Assert // Reader should be garbage collected by now Assert.False(weakReader.IsAlive); @@ -205,7 +175,7 @@ public void ExecuteReader_GarbageCollection_Wait(CommandType commandType) public void ExecuteReader_GarbageCollection_NoWait(CommandType commandType) { // Arrange - using SqlConnection connection = new SqlConnection(_testConnString); + using SqlConnection connection = new SqlConnection(TestConnString); using DisposableArray commands = GetCommands(connection, commandType); connection.Open(); @@ -221,10 +191,6 @@ public void ExecuteReader_GarbageCollection_NoWait(CommandType commandType) // Run the garbage collector, but do not wait for finalization GC.Collect(); - // Close/reopen connection to force disposal of sessions - connection.Close(); - connection.Open(); - // Assert AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); } @@ -236,7 +202,7 @@ public void ExecuteReader_GarbageCollection_NoWait(CommandType commandType) public void ExecuteReader_NoCloses(CommandType commandType) { // Arrange - using SqlConnection connection = new SqlConnection(_testConnString); + using SqlConnection connection = new SqlConnection(TestConnString); using DisposableArray commands = GetCommands(connection, commandType); using DisposableArray readers = new DisposableArray(commands.Length); connection.Open(); @@ -244,54 +210,35 @@ public void ExecuteReader_NoCloses(CommandType commandType) // Act / Assert for (int i = 0; i < commands.Length; i++) { - using SqlDataReader reader = commands[i].ExecuteReader(); - GC.SuppressFinalize(reader); + // Act + // Run command, close nothing! + readers[i] = commands[i].ExecuteReader(); + GC.SuppressFinalize(readers[i]); - // // Act - // // Run command, close nothing! - // readers[i] = commands[i].ExecuteReader(); - // GC.SuppressFinalize(readers[i]); - // - // // Assert - // // MARS session for all previous commands should still be open - // // Sessions: 1 for connection, i+1 for previous commands (with 0-index offset) - // // Requests: i+1 for previous commands (with 0-index offset) - // AssertSessionsAndRequests(connection, expectedSessions: i + 2, expectedRequests: i + 1); - AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); + // Assert + // MARS session for all previous commands should still be open + // Sessions: 1 for connection, i+1 for previous commands (with 0-index offset) + // Requests: i+1 for previous commands (with 0-index offset) + AssertSessionsAndRequests(connection, expectedSessions: i + 2, expectedRequests: i + 1); } - // foreach (var q in readers) - // { - // q.Close(); - // q.Dispose(); - // } - // - // foreach (var q in commands) - // { - // q.Dispose(); - // } - // - // connection.Close(); - // connection.Open(); - // connection.Dispose(); + // @TODO: THIS POISONS THE POOL. IS THIS A BUG??? } - private DisposableArray GetCommands(SqlConnection connection, CommandType commandType) + private static DisposableArray GetCommands(SqlConnection connection, CommandType commandType) { - SqlCommand[] result = new SqlCommand[CONCURRENT_COMMANDS]; + SqlCommand[] result = new SqlCommand[ConcurrentCommands]; for (int i = 0; i < result.Length; i++) { switch (commandType) { case CommandType.Text: + string commandText = string.Join(" ", Enumerable.Repeat(@"SELECT * FROM sys.databases;", 20)); + commandText += @" PRINT 'THIS IS THE END!'"; + result[i] = new SqlCommand { - CommandText = - "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + - "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + - "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + - "select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; select * from sys.databases; " + - "select * from sys.databases; print 'THIS IS THE END!'", + CommandText = commandText, CommandTimeout = 120, CommandType = CommandType.Text, Connection = connection @@ -346,184 +293,6 @@ private void AssertSessionsAndRequests(SqlConnection connection, int expectedSes Assert.Equal(expectedRequests + 1, reader.GetInt32(0)); } - - private enum ExecuteType - { - ExecuteScalar, - ExecuteNonQuery, - ExecuteReader, - } - - private enum ReaderTestType - { - ReaderClose, - ReaderDispose, - ReaderGC, - ConnectionClose, - NoCloses, - } - - private enum GCType - { - Wait, - NoWait, - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void TestMARSSessionPooling(string caseName, string connectionString, CommandType commandType, - ExecuteType executeType, ReaderTestType readerTestType, GCType gcType) - { - SqlCommand[] cmd = new SqlCommand[CONCURRENT_COMMANDS]; - SqlDataReader[] gch = new SqlDataReader[CONCURRENT_COMMANDS]; - - using (SqlConnection con = new SqlConnection(connectionString)) - { - con.Open(); - - // Create command - for (int i = 0; i < CONCURRENT_COMMANDS; i++) - { - // Prepare all commands - cmd[i] = con.CreateCommand(); - switch (commandType) - { - case CommandType.Text: - cmd[i].CommandText = _COMMAND_SQL; - cmd[i].CommandTimeout = 120; - break; - case CommandType.StoredProcedure: - cmd[i].CommandText = _COMMAND_RPC; - cmd[i].CommandTimeout = 120; - cmd[i].CommandType = CommandType.StoredProcedure; - break; - } - } - - for (int i = 0; i < CONCURRENT_COMMANDS; i++) - { - switch (executeType) - { - // case ExecuteType.ExecuteScalar: - // cmd[i].ExecuteScalar(); - // break; - // case ExecuteType.ExecuteNonQuery: - // cmd[i].ExecuteNonQuery(); - // break; - case ExecuteType.ExecuteReader: - if (readerTestType != ReaderTestType.ReaderGC) - { - gch[i] = cmd[i].ExecuteReader(); - } - - switch (readerTestType) - { - // case ReaderTestType.ReaderClose: - // { - // gch[i].Dispose(); - // break; - // } - // case ReaderTestType.ReaderDispose: - // gch[i].Dispose(); - // break; - // case ReaderTestType.ReaderGC: - // // gch[i] = null; - // // WeakReference weak = OpenReaderThenNullify(cmd[i]); - // // GC.Collect(); - // - // // if (gcType == GCType.Wait) - // // { - // // GC.WaitForPendingFinalizers(); - // // Assert.False(weak.IsAlive, "Error - target still alive!"); - // // } - // break; - // case ReaderTestType.ConnectionClose: - // GC.SuppressFinalize(gch[i]); - // con.Close(); - // con.Open(); - // break; - case ReaderTestType.NoCloses: - GC.SuppressFinalize(gch[i]); - break; - } - break; - } - - if (readerTestType != ReaderTestType.NoCloses) - { - con.Close(); - con.Open(); // Close and open, to re-assure collection! - } - - using (SqlCommand verificationCmd = con.CreateCommand()) - { - - verificationCmd.CommandText = COMMAND_STATUS; - using (SqlDataReader rdr = verificationCmd.ExecuteReader()) - { - rdr.Read(); - int connections = (int)rdr.GetValue(0); - int spid1 = (Int16)rdr.GetValue(1); - rdr.NextResult(); - rdr.Read(); - int requests = (int)rdr.GetValue(0); - int spid2 = (Int16)rdr.GetValue(1); - - switch (executeType) - { - // case ExecuteType.ExecuteScalar: - // case ExecuteType.ExecuteNonQuery: - // // 1 for connection, 1 for command - // Assert.True(connections == 2, "Failure - incorrect number of connections for ExecuteScalar! #connections: " + connections); - // - // // only 1 executing - // Assert.True(requests == 1, "Failure - incorrect number of requests for ExecuteScalar! #requests: " + requests); - // break; - case ExecuteType.ExecuteReader: - switch (readerTestType) - { - // case ReaderTestType.ReaderClose: - // case ReaderTestType.ReaderDispose: - // case ReaderTestType.ConnectionClose: - // // 1 for connection, 1 for command - // Assert.True(connections == 2, "Failure - Incorrect number of connections for ReaderClose / ReaderDispose / ConnectionClose! #connections: " + connections); - // - // // only 1 executing - // Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderClose/ReaderDispose/ConnectionClose! #requests: " + requests); - // break; - // case ReaderTestType.ReaderGC: - // switch (gcType) - // { - // // case GCType.Wait: - // // // 1 for connection, 1 for open reader - // // Assert.True(connections == 2, "Failure - incorrect number of connections for ReaderGCWait! #connections: " + connections); - // // // only 1 executing - // // Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderGCWait! #requests: " + requests); - // // break; - // case GCType.NoWait: - // // 1 for connection, 1 for open reader - // Assert.True(connections == 2, "Failure - incorrect number of connections for ReaderGCNoWait! #connections: " + connections); - // - // // only 1 executing - // Assert.True(requests == 1, "Failure - incorrect number of requests for ReaderGCNoWait! #requests: " + requests); - // break; - // } - // break; - case ReaderTestType.NoCloses: - // 1 for connection, 1 for current command, 1 for 0 based array offset, plus i for open readers - Assert.Equal(3+i, connections); - - // 1 for current command, 1 for 0 based array offset, plus i open readers - Assert.Equal(2+i, requests); - break; - } - break; - } - } - } - } - } - } - private static WeakReference OpenReaderThenNullify(SqlCommand command) { SqlDataReader reader = command.ExecuteReader(); From 814736faeba79489e60305b2fa17ead1b89d08a2 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 10 Oct 2025 11:15:48 -0500 Subject: [PATCH 08/15] Clean up a biiiit more --- .../MARSSessionPoolingTest.cs | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs index 4c58a1ddf2..a857079e04 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs @@ -225,6 +225,40 @@ public void ExecuteReader_NoCloses(CommandType commandType) // @TODO: THIS POISONS THE POOL. IS THIS A BUG??? } + private static void AssertSessionsAndRequests(SqlConnection connection, int expectedSessions, int expectedRequests) + { + using SqlCommand verificationCommand = new SqlCommand(); + verificationCommand.CommandText = + @"SELECT COUNT(*) AS MarsSessionCount " + + @"FROM sys.dm_exec_connections " + + @"WHERE session_id=@@spid AND net_transport='Session'; " + + @"SELECT COUNT(*) as ActiveRequestCount " + + @"FROM sys.dm_exec_requests " + + @"WHERE session_id=@@spid AND (status='running' OR status='suspended')"; + verificationCommand.CommandType = CommandType.Text; + verificationCommand.Connection = connection; + + using SqlDataReader reader = verificationCommand.ExecuteReader(); + + // Result 1) Count of active MARS sessions from sys.dm_exec_connections + if (!reader.Read()) + { + throw new Exception("Expected dm_exec_connections results from verification command"); + } + + // Add 1 for the verification command executing + Assert.Equal(expectedSessions + 1, reader.GetInt32(0)); + + // Result 2) Count of active requests from sys.dm_exec_requests + if (!reader.NextResult() || !reader.Read()) + { + throw new Exception("Expected dm_exec_requests results from verification command"); + } + + // Add 1 for the verification command executing + Assert.Equal(expectedRequests + 1, reader.GetInt32(0)); + } + private static DisposableArray GetCommands(SqlConnection connection, CommandType commandType) { SqlCommand[] result = new SqlCommand[ConcurrentCommands]; @@ -263,36 +297,6 @@ private static DisposableArray GetCommands(SqlConnection connection, return new DisposableArray(result); } - private void AssertSessionsAndRequests(SqlConnection connection, int expectedSessions, int expectedRequests) - { - using SqlCommand verificationCommand = new SqlCommand(); - verificationCommand.CommandText = - "select count(*) as MarsSessionCount from sys.dm_exec_connections where session_id=@@spid and net_transport='Session'; " + - "select count(*) as ActiveRequestCount from sys.dm_exec_requests where session_id=@@spid and (status='running' or status='suspended')"; - verificationCommand.CommandType = CommandType.Text; - verificationCommand.Connection = connection; - - using SqlDataReader reader = verificationCommand.ExecuteReader(); - - // Result 1) Count of active MARS sessions from sys.dm_exec_connections - if (!reader.Read()) - { - throw new Exception("Expected dm_exec_connections results from verification command"); - } - - // Add 1 for the verification command executing - Assert.Equal(expectedSessions + 1, reader.GetInt32(0)); - - // Result 2) Count of active requests from sys.dm_exec_requests - if (!reader.NextResult() || !reader.Read()) - { - throw new Exception("Expected dm_exec_requests results from verification command"); - } - - // Add 1 for the verification command executing - Assert.Equal(expectedRequests + 1, reader.GetInt32(0)); - } - private static WeakReference OpenReaderThenNullify(SqlCommand command) { SqlDataReader reader = command.ExecuteReader(); From d2a793076d4b88ebd61fef1ef17dacc1f62abf84 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 5 Nov 2025 12:16:51 -0600 Subject: [PATCH 09/15] Finishing test matrix, improving DisposableArray, renaming test file to match guidelines --- .../tests/Common/DisposableArray.cs | 2 + ....Data.SqlClient.ManualTesting.Tests.csproj | 2 +- .../MARSSessionPoolingTest.cs | 308 ------------- .../MarsSessionPoolingTest.cs | 429 ++++++++++++++++++ 4 files changed, 432 insertions(+), 309 deletions(-) delete mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs diff --git a/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs b/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs index bb4ec95e1c..50830f8271 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs @@ -37,6 +37,8 @@ public void Dispose() { element?.Dispose(); } + + GC.SuppressFinalize(this); } public IEnumerator GetEnumerator() => 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 fbaf55db72..f7f8854553 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 @@ -192,7 +192,7 @@ - + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs deleted file mode 100644 index a857079e04..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs +++ /dev/null @@ -1,308 +0,0 @@ -// 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.Data; -using System.Linq; -using System.Runtime.CompilerServices; -using Microsoft.Data.SqlClient.Tests.Common; -using Xunit; - -namespace Microsoft.Data.SqlClient.ManualTesting.Tests -{ - public class MARSSessionPoolingTest - { - private const int ConcurrentCommands = 5; - private static readonly string TestConnString = - new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) - { - ApplicationName = "SqlClientMarsPoolingTests", // Specify application name to make these tests unique - // for pooling purposes. - PacketSize = 512, - MaxPoolSize = 1, - MultipleActiveResultSets = true - }.ConnectionString; - - // Synapse: Catalog view 'dm_exec_connections' is not supported in this version. - - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] - [InlineData(CommandType.Text)] - [InlineData(CommandType.StoredProcedure)] - public void ExecuteScalar(CommandType commandType) - { - // Arrange - using SqlConnection connection = new SqlConnection(TestConnString); - using DisposableArray commands = GetCommands(connection, commandType); - connection.Open(); - - // Act / Assert - foreach (SqlCommand command in commands) - { - // Act - // Run command - command.ExecuteScalar(); - - // Assert - AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); - } - } - - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] - [InlineData(CommandType.Text)] - [InlineData(CommandType.StoredProcedure)] - public void ExecuteNonQuery(CommandType commandType) - { - // Arrange - using SqlConnection connection = new SqlConnection(TestConnString); - using DisposableArray commands = GetCommands(connection, commandType); - connection.Open(); - - // Act / Assert - foreach (SqlCommand command in commands) - { - // Act - // Run command - command.ExecuteScalar(); - - // Assert - AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); - } - } - - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] - [InlineData(CommandType.Text)] - [InlineData(CommandType.StoredProcedure)] - public void ExecuteReader_CloseReader(CommandType commandType) - { - // Arrange - using SqlConnection connection = new SqlConnection(TestConnString); - using DisposableArray commands = GetCommands(connection, commandType); - connection.Open(); - - // Act / Assert - foreach (SqlCommand command in commands) - { - // Act - // Run command, close reader - using SqlDataReader reader = command.ExecuteReader(); - reader.Close(); - - // Assert - AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); - } - } - - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] - [InlineData(CommandType.Text)] - [InlineData(CommandType.StoredProcedure)] - public void ExecuteReader_DisposeReader(CommandType commandType) - { - // Arrange - using SqlConnection connection = new SqlConnection(TestConnString); - using DisposableArray commands = GetCommands(connection, commandType); - connection.Open(); - - // Act / Assert - foreach (SqlCommand command in commands) - { - // Act - // Run command, dispose reader - SqlDataReader reader = command.ExecuteReader(); - reader.Dispose(); - - // Assert - AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); - } - } - - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] - [InlineData(CommandType.Text)] - [InlineData(CommandType.StoredProcedure)] - public void ExecuteReader_CloseConnection(CommandType commandType) - { - // Arrange - using SqlConnection connection = new SqlConnection(TestConnString); - using DisposableArray commands = GetCommands(connection, commandType); - connection.Open(); - - // Act / Assert - foreach (SqlCommand command in commands) - { - // Act - // Run command, suppress finalization of reader - using SqlDataReader reader = command.ExecuteReader(); - GC.SuppressFinalize(reader); - - // Assert - AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); - } - } - - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] - [InlineData(CommandType.Text)] - [InlineData(CommandType.StoredProcedure)] - public void ExecuteReader_GarbageCollection_Wait(CommandType commandType) - { - // Arrange - using SqlConnection connection = new SqlConnection(TestConnString); - using DisposableArray commands = GetCommands(connection, commandType); - connection.Open(); - - // Act / Assert - foreach (SqlCommand command in commands) - { - // Act - // Run command and get weak reference to reader - // Note: This must happen in another scope otherwise the reader will not be marked - // for garbage collection - WeakReference weakReader = OpenReaderThenNullify(command); - - // Run the garbage collector to force cleanup of reader - GC.Collect(); - GC.WaitForPendingFinalizers(); - - // Assert - // Reader should be garbage collected by now - Assert.False(weakReader.IsAlive); - AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); - } - } - - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] - [InlineData(CommandType.Text)] - [InlineData(CommandType.StoredProcedure)] - public void ExecuteReader_GarbageCollection_NoWait(CommandType commandType) - { - // Arrange - using SqlConnection connection = new SqlConnection(TestConnString); - using DisposableArray commands = GetCommands(connection, commandType); - connection.Open(); - - // Act / Assert - foreach (SqlCommand command in commands) - { - // Act - // Run command - // Note: This must happen in another scope otherwise the reader will not be marked - // for garbage collection - _ = OpenReaderThenNullify(command); - - // Run the garbage collector, but do not wait for finalization - GC.Collect(); - - // Assert - AssertSessionsAndRequests(connection, expectedSessions: 1, expectedRequests: 0); - } - } - - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsNotAzureSynapse))] - [InlineData(CommandType.Text)] - [InlineData(CommandType.StoredProcedure)] - public void ExecuteReader_NoCloses(CommandType commandType) - { - // Arrange - using SqlConnection connection = new SqlConnection(TestConnString); - using DisposableArray commands = GetCommands(connection, commandType); - using DisposableArray readers = new DisposableArray(commands.Length); - connection.Open(); - - // Act / Assert - for (int i = 0; i < commands.Length; i++) - { - // Act - // Run command, close nothing! - readers[i] = commands[i].ExecuteReader(); - GC.SuppressFinalize(readers[i]); - - // Assert - // MARS session for all previous commands should still be open - // Sessions: 1 for connection, i+1 for previous commands (with 0-index offset) - // Requests: i+1 for previous commands (with 0-index offset) - AssertSessionsAndRequests(connection, expectedSessions: i + 2, expectedRequests: i + 1); - } - - // @TODO: THIS POISONS THE POOL. IS THIS A BUG??? - } - - private static void AssertSessionsAndRequests(SqlConnection connection, int expectedSessions, int expectedRequests) - { - using SqlCommand verificationCommand = new SqlCommand(); - verificationCommand.CommandText = - @"SELECT COUNT(*) AS MarsSessionCount " + - @"FROM sys.dm_exec_connections " + - @"WHERE session_id=@@spid AND net_transport='Session'; " + - @"SELECT COUNT(*) as ActiveRequestCount " + - @"FROM sys.dm_exec_requests " + - @"WHERE session_id=@@spid AND (status='running' OR status='suspended')"; - verificationCommand.CommandType = CommandType.Text; - verificationCommand.Connection = connection; - - using SqlDataReader reader = verificationCommand.ExecuteReader(); - - // Result 1) Count of active MARS sessions from sys.dm_exec_connections - if (!reader.Read()) - { - throw new Exception("Expected dm_exec_connections results from verification command"); - } - - // Add 1 for the verification command executing - Assert.Equal(expectedSessions + 1, reader.GetInt32(0)); - - // Result 2) Count of active requests from sys.dm_exec_requests - if (!reader.NextResult() || !reader.Read()) - { - throw new Exception("Expected dm_exec_requests results from verification command"); - } - - // Add 1 for the verification command executing - Assert.Equal(expectedRequests + 1, reader.GetInt32(0)); - } - - private static DisposableArray GetCommands(SqlConnection connection, CommandType commandType) - { - SqlCommand[] result = new SqlCommand[ConcurrentCommands]; - for (int i = 0; i < result.Length; i++) - { - switch (commandType) - { - case CommandType.Text: - string commandText = string.Join(" ", Enumerable.Repeat(@"SELECT * FROM sys.databases;", 20)); - commandText += @" PRINT 'THIS IS THE END!'"; - - result[i] = new SqlCommand - { - CommandText = commandText, - CommandTimeout = 120, - CommandType = CommandType.Text, - Connection = connection - }; - break; - - case CommandType.StoredProcedure: - result[i] = new SqlCommand - { - CommandText = "sp_who", - CommandTimeout = 120, - CommandType = CommandType.StoredProcedure, - Connection = connection - }; - break; - - default: - throw new InvalidOperationException("Not supported test type"); - } - } - - return new DisposableArray(result); - } - - private static WeakReference OpenReaderThenNullify(SqlCommand command) - { - SqlDataReader reader = command.ExecuteReader(); - WeakReference weak = new WeakReference(reader); - reader = null; - return weak; - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs new file mode 100644 index 0000000000..54f5b7931c --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs @@ -0,0 +1,429 @@ +// 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.Data; +using System.Linq; +using Microsoft.Data.SqlClient.Tests.Common; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class MarsSessionPoolingTest + { + private const int ConcurrentCommands = 5; + + // Synapse: Catalog view 'dm_exec_connections' is not supported in this version. + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteScalar_DisposeCommand(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // - Run command + command.ExecuteScalar(); + + // - Dispose command + command.Dispose(); + + // Assert + // - Count of sessions/requests should stay the same + // Each request runs to completion, none are running concurrently, so additional + // MARS sessions do not need to be opened. + AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteScalar_CloseConnection(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // - Run command + command.ExecuteScalar(); + + // - Close and reopen connection (return to pool) + connection.Close(); + connection.Open(); + + // Assert + // - Count of sessions/requests should stay the same + // Each request runs to completion, none are running concurrently, so additional + // MARS sessions do not need to be opened. + AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0); + } + } + + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteNonQuery_DisposeCommand(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // - Run command + command.ExecuteScalar(); + + // - Dispose command + command.Dispose(); + + // Assert + // - Count of sessions/requests should stay the same + // Each request runs to completion, none are running concurrently, so additional + // MARS sessions do not need to be opened. + AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteNonQuery_CloseConnection(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + foreach (SqlCommand command in commands) + { + // Act + // - Run command + command.ExecuteNonQuery(); + + // - Close and reopen connection (return to pool) + connection.Close(); + connection.Open(); + + // Assert + // - Count of sessions/requests should stay the same + // Each request runs to completion, none are running concurrently, so additional + // MARS sessions do not need to be opened. + AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_CloseReader(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + using DisposableArray readers = new DisposableArray(commands.Length); + connection.Open(); + + // Act / Assert + for (int i = 0; i < commands.Length; i++) + { + // Act + // - Run command + readers[i] = commands[i].ExecuteReader(); + + // - Close reader + readers[i].Close(); + + // Assert + // - Count of sessions/requests should stay the same + // Closing the reader completes the request, so no requests are running + // concurrently, so no additional MARS sessions should have been opened. + AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_DisposeReader(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + using DisposableArray readers = new DisposableArray(commands.Length); + connection.Open(); + + // Act / Assert + for (int i = 0; i < commands.Length; i++) + { + // Act + // - Run command + readers[i] = commands[i].ExecuteReader(); + + // - Dispose reader + readers[i].Dispose(); + + // Assert + // - Count of sessions/requests should stay the same + // Disposing the reader completes the request, so no requests are running + // concurrently, so no additional MARS sessions should have been opened. + AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_GarbageCollectReader(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + connection.Open(); + + // Act / Assert + for (int i = 0; i < commands.Length; i++) + { + // Act + // - Run command and get weak reference to reader. + // This must happen in another scope otherwise the reader will not be marked for + // garbage collection. + WeakReference readerWeakReference = OpenReaderThenNullify(commands[i]); + + // - Run the garbage collector + GC.Collect(); + GC.WaitForPendingFinalizers(); + + // Assert + // - Make sure reader has been collected by now, otherwise results are invalid + Assert.False(readerWeakReference.IsAlive); + + // - Count of open sessions/requests will increase with each iteration + // Finalizing a data reader does *not* close it, meaning the MARS session is left + // in an incomplete state. As such, with each command that's executed, a new + // session is opened. + AssertSessionsAndRequests(connection, openMarsSessions: i + 1, openRequests: i + 1); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_DisposeCommand(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + using DisposableArray readers = new DisposableArray(commands.Length); + connection.Open(); + + // Act / Assert + for (int i = 0; i < commands.Length; i++) + { + // Act + // - Run command + readers[i] = commands[i].ExecuteReader(); + + // - Dispose the command + commands[i].Dispose(); + + // Assert + // - Count of open sessions/requests will increase with each iteration + // Disposing of the command does *not* close the reader, meaning the MARS session + // is left in an incomplete state. As such, with each command that's executed, a + // new session is opened. + AssertSessionsAndRequests(connection, openMarsSessions: i + 1, openRequests: i + 1); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_CloseConnection(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + using DisposableArray readers = new DisposableArray(commands.Length); + connection.Open(); + + // Act / Assert + for (int i = 0; i < commands.Length; i++) + { + // Act + // - Run command + readers[i] = commands[i].ExecuteReader(); + + // - Close and reopen connection (return to pool) + connection.Close(); + connection.Open(); + + // Assert + // - Count of sessions/requests should stay the same + // Closing the connection completes any pending requests, so no requests are + // running concurrently, so no additional MARS sessions should have been opened. + AssertSessionsAndRequests(connection, openMarsSessions: 0, openRequests: 0); + } + } + + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] + [InlineData(CommandType.Text)] + [InlineData(CommandType.StoredProcedure)] + public void ExecuteReader_NoCloses(CommandType commandType) + { + // Arrange + using SqlConnection connection = GetConnection(); + using DisposableArray commands = GetCommands(connection, commandType); + using DisposableArray readers = new DisposableArray(commands.Length); + connection.Open(); + + // Act / Assert + for (int i = 0; i < commands.Length; i++) + { + // Act + // - Run command, close nothing! + readers[i] = commands[i].ExecuteReader(); + + // Assert + // - Count of open sessions/requests will increase with each iteration + // Leaving a data reader open leaves the MARS session in an incomplete state. As + // such, with each command that's executed, a new session is opened. + AssertSessionsAndRequests(connection, openMarsSessions: i + 1, openRequests: i + 1); + } + } + + /// + /// Asserts the number of open sessions and pending requests on the connection. + /// + /// Connection to check open sessions and pending requests on + /// + /// Number of MARS sessions expected to be open/in use by the test. The sessions for the + /// main connection and validation query will be added before assertion. + /// + /// + /// Number of open requests expected to be pending by use of the test. The request for the + /// validation query will be added before assertion. + /// + /// + /// Thrown if any of the validation result sets are missing or empty. + /// + private static void AssertSessionsAndRequests( + SqlConnection connection, + int openMarsSessions, + int openRequests) + { + using SqlCommand verificationCommand = new SqlCommand(); + verificationCommand.CommandText = + @"SELECT COUNT(*) AS SessionCount " + + @"FROM sys.dm_exec_connections " + + @"WHERE session_id=@@spid AND net_transport='Session'; " + + @"SELECT COUNT(*) AS RequestCount " + + @"FROM sys.dm_exec_requests " + + @"WHERE session_id=@@spid AND (status='running' OR status='suspended')"; + verificationCommand.CommandType = CommandType.Text; + verificationCommand.Connection = connection; + + using SqlDataReader reader = verificationCommand.ExecuteReader(); + + // Result 1) Count of active MARS sessions from sys.dm_exec_connections + if (!reader.Read()) + { + throw new Exception("Expected dm_exec_connections results from verification command"); + } + + // For these tests, the expected session count will always be at least 2 in MARS mode: + // 1 for the main connection + // 1 for the verification command we just executed + Assert.Equal(openMarsSessions + 2, reader.GetInt32(0)); + + // Result 2) Count of active requests from sys.dm_exec_requests + if (!reader.NextResult() || !reader.Read()) + { + throw new Exception("Expected dm_exec_requests results from verification command"); + } + + // For these tests, the expected session count will always be at least 1: + // 1 for the verification command we just executed + Assert.Equal(openRequests + 1, reader.GetInt32(0)); + } + + private static DisposableArray GetCommands(SqlConnection connection, CommandType commandType) + { + SqlCommand[] result = new SqlCommand[ConcurrentCommands]; + for (int i = 0; i < result.Length; i++) + { + switch (commandType) + { + case CommandType.Text: + string commandText = string.Join(" ", Enumerable.Repeat(@"SELECT * FROM sys.databases;", 20)); + commandText += @" PRINT 'THIS IS THE END!'"; + + result[i] = new SqlCommand + { + CommandText = commandText, + CommandTimeout = 120, + CommandType = CommandType.Text, + Connection = connection + }; + break; + + case CommandType.StoredProcedure: + result[i] = new SqlCommand + { + CommandText = "sp_who", + CommandTimeout = 120, + CommandType = CommandType.StoredProcedure, + Connection = connection + }; + break; + + default: + throw new InvalidOperationException("Not supported test type"); + } + } + + return new DisposableArray(result); + } + + private static SqlConnection GetConnection() + { + // Generate a unique name for the application to ensure pool isolation between tests + string applicationName = $"SqlClientMarsPoolingTests:{Guid.NewGuid()}"; + string connectionString = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) + { + ApplicationName = applicationName, + PacketSize = 512, + MaxPoolSize = 1, + MultipleActiveResultSets = true + }.ConnectionString; + + return new SqlConnection(connectionString); + } + + private static WeakReference OpenReaderThenNullify(SqlCommand command) + { + SqlDataReader reader = command.ExecuteReader(); + WeakReference weak = new WeakReference(reader); + reader = null; + return weak; + } + } +} From 099bc0b846319b835d36c5857e00ef0b34c23087 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 6 Nov 2025 16:02:17 -0600 Subject: [PATCH 10/15] Final iteration of tests, adding backoff to DMV queries, adding #nullable enable --- .../MarsSessionPoolingTest.cs | 90 +++++++++++++------ 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs index 54f5b7931c..ccc19d4a60 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs @@ -5,9 +5,12 @@ using System; using System.Data; using System.Linq; +using System.Threading; using Microsoft.Data.SqlClient.Tests.Common; using Xunit; +#nullable enable + namespace Microsoft.Data.SqlClient.ManualTesting.Tests { public class MarsSessionPoolingTest @@ -73,7 +76,6 @@ public void ExecuteScalar_CloseConnection(CommandType commandType) } } - [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsNotAzureSynapse), nameof(DataTestUtility.IsNotManagedInstance))] [InlineData(CommandType.Text)] [InlineData(CommandType.StoredProcedure)] @@ -271,7 +273,6 @@ public void ExecuteReader_CloseConnection(CommandType commandType) // Act // - Run command readers[i] = commands[i].ExecuteReader(); - // - Close and reopen connection (return to pool) connection.Close(); connection.Open(); @@ -330,39 +331,39 @@ private static void AssertSessionsAndRequests( int openMarsSessions, int openRequests) { - using SqlCommand verificationCommand = new SqlCommand(); - verificationCommand.CommandText = - @"SELECT COUNT(*) AS SessionCount " + - @"FROM sys.dm_exec_connections " + - @"WHERE session_id=@@spid AND net_transport='Session'; " + - @"SELECT COUNT(*) AS RequestCount " + - @"FROM sys.dm_exec_requests " + - @"WHERE session_id=@@spid AND (status='running' OR status='suspended')"; - verificationCommand.CommandType = CommandType.Text; - verificationCommand.Connection = connection; - - using SqlDataReader reader = verificationCommand.ExecuteReader(); - - // Result 1) Count of active MARS sessions from sys.dm_exec_connections - if (!reader.Read()) - { - throw new Exception("Expected dm_exec_connections results from verification command"); - } + const int maxAttempts = 5; // For these tests, the expected session count will always be at least 2 in MARS mode: // 1 for the main connection // 1 for the verification command we just executed - Assert.Equal(openMarsSessions + 2, reader.GetInt32(0)); + int? observedSessions = null; + int expectedSessions = openMarsSessions + 2; - // Result 2) Count of active requests from sys.dm_exec_requests - if (!reader.NextResult() || !reader.Read()) + // For these tests, the expected session count will always be at least 1: + // 1 for the verification command we just executed + int? observedRequests = null; + int expectedRequests = openRequests + 1; + + // There is a race between opening new sessions and them appearing in the DMV tables. + // As such, we want to poll the DMV a few times before declaring the wrong behavior was + // observed. + for (int attempt = 0; attempt < maxAttempts; attempt++) { - throw new Exception("Expected dm_exec_requests results from verification command"); + (observedSessions, observedRequests) = QuerySessionCounters(connection); + if (observedSessions == expectedSessions && observedSessions == expectedRequests) + { + // We observed the expected values. + return; + } + + // Back off and wait before trying again + Thread.SpinWait(20 << attempt); } - // For these tests, the expected session count will always be at least 1: - // 1 for the verification command we just executed - Assert.Equal(openRequests + 1, reader.GetInt32(0)); + // If we make it to here, we never saw the expected numbers, so fail the test with the + // last value we observed. + Assert.Equal(expectedSessions, observedSessions); + Assert.Equal(expectedRequests, observedRequests); } private static DisposableArray GetCommands(SqlConnection connection, CommandType commandType) @@ -420,10 +421,43 @@ private static SqlConnection GetConnection() private static WeakReference OpenReaderThenNullify(SqlCommand command) { - SqlDataReader reader = command.ExecuteReader(); + SqlDataReader? reader = command.ExecuteReader(); WeakReference weak = new WeakReference(reader); reader = null; return weak; } + + private static (int sessions, int requests) QuerySessionCounters(SqlConnection connection) + { + using SqlCommand verificationCommand = new SqlCommand(); + verificationCommand.CommandText = + @"SELECT COUNT(*) AS SessionCount " + + @"FROM sys.dm_exec_connections " + + @"WHERE session_id=@@spid AND net_transport='Session'; " + + @"SELECT COUNT(*) AS RequestCount " + + @"FROM sys.dm_exec_requests " + + @"WHERE session_id=@@spid AND (status='running' OR status='suspended')"; + verificationCommand.CommandType = CommandType.Text; + verificationCommand.Connection = connection; + + // Result 1) Count of active sessions from sys.dm_exec_connections + using SqlDataReader reader = verificationCommand.ExecuteReader(); + if (!reader.Read()) + { + throw new Exception("Expected dm_exec_connections results from verification command"); + } + + int sessions = reader.GetInt32(0); + + // Result 2) Count of active requests from sys.dm_exec_requests + if (!reader.NextResult() || !reader.Read()) + { + throw new Exception("Expected dm_exec_requests results from verification command"); + } + + int requests = reader.GetInt32(0); + + return (sessions, requests); + } } } From 976c6fe43d93d46dda2719d52ded55c63c61dfe7 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 6 Nov 2025 17:53:19 -0600 Subject: [PATCH 11/15] Fix mistakes in MARS tests --- .../SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs index ccc19d4a60..9dfa275688 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs @@ -91,7 +91,7 @@ public void ExecuteNonQuery_DisposeCommand(CommandType commandType) { // Act // - Run command - command.ExecuteScalar(); + command.ExecuteNonQuery(); // - Dispose command command.Dispose(); @@ -350,7 +350,7 @@ private static void AssertSessionsAndRequests( for (int attempt = 0; attempt < maxAttempts; attempt++) { (observedSessions, observedRequests) = QuerySessionCounters(connection); - if (observedSessions == expectedSessions && observedSessions == expectedRequests) + if (observedSessions == expectedSessions && observedRequests == expectedRequests) { // We observed the expected values. return; From f17d382547afd414b56863ee47ec145275fb9b0c Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 6 Nov 2025 18:16:03 -0600 Subject: [PATCH 12/15] Rewrite TimeoutCancel to use less custom utilities - hopefully this will make it easier to sort out why we have these failures --- .../SQL/SqlCommand/SqlCommandCancelTest.cs | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs index 7717bc39c9..729b66aad5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs @@ -345,26 +345,27 @@ private static void MultiThreadedCancel(string constr, bool async) private static void TimeoutCancel(string constr) { - using (SqlConnection con = new SqlConnection(constr)) - { - con.Open(); - using (SqlCommand cmd = con.CreateCommand()) - { - cmd.CommandTimeout = 1; - cmd.CommandText = "WAITFOR DELAY '00:00:20';select * from Customers"; - - string errorMessage = SystemDataResourceManager.Instance.SQL_Timeout_Execution; - DataTestUtility.ExpectFailure(() => ExecuteReaderOnCmd(cmd), new string[] { errorMessage }); - - VerifyConnection(cmd); - } - } - } - - private static void ExecuteReaderOnCmd(SqlCommand cmd) - { - using (SqlDataReader reader = cmd.ExecuteReader()) - { } + // Arrange + using SqlConnection connection = new SqlConnection(constr); + connection.Open(); + + using SqlCommand command = new SqlCommand(); + command.CommandTimeout = 1; + command.CommandText = @"WAITFOR DELAY '00:01:00';" + + @"SELECT * FROM customers"; + command.CommandType = CommandType.Text; + command.Connection = connection; + + // Act + Action action = () => { using SqlDataReader reader = command.ExecuteReader(); }; + + // Assert + // - Action throws timeout exception with timeout message + Exception e = Assert.Throws(action); + Assert.Contains(SystemDataResourceManager.Instance.SQL_Timeout_Execution, e.Message); + + // - Connection has not faulted + VerifyConnection(command); } //InvalidOperationException from connection.Dispose if that connection has prepared command cancelled during reading of data From 9dec519106e4d91dd1209aace33503cf94e65fbb Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 7 Nov 2025 11:45:33 -0600 Subject: [PATCH 13/15] Fix session => request comment --- .../SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs index 9dfa275688..687ae1696e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs @@ -339,7 +339,7 @@ private static void AssertSessionsAndRequests( int? observedSessions = null; int expectedSessions = openMarsSessions + 2; - // For these tests, the expected session count will always be at least 1: + // For these tests, the expected request count will always be at least 1: // 1 for the verification command we just executed int? observedRequests = null; int expectedRequests = openRequests + 1; From 55887af45dd980d2f40ae39641dd91b0ad36a90e Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 10 Nov 2025 17:35:15 -0600 Subject: [PATCH 14/15] Re-disable TimeoutCancelNamedPipe due to bug in managed SNI implementation --- .../tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs index 729b66aad5..9f20521f08 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs @@ -157,6 +157,7 @@ public static void TimeoutCancelTcp() TimeoutCancel(tcp_connStr); } + [ActiveIssue("https://github.com/dotnet/SqlClient/issues/3755")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] [PlatformSpecific(TestPlatforms.Windows)] public static void TimeoutCancelNamedPipe() From 27e9f3d43a1af2d842d4b239f8ca706bcac1c34f Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 11 Nov 2025 17:51:57 -0600 Subject: [PATCH 15/15] Remove AreConnStringsNotAzureSynapse Adding doc comments to DisposableArray --- .../tests/Common/DisposableArray.cs | 51 +++++++++++++++---- .../ManualTests/DataCommon/DataTestUtility.cs | 3 -- .../MarsSessionPoolingTest.cs | 4 +- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs b/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs index 50830f8271..a5ca8e639a 100644 --- a/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs +++ b/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs @@ -6,31 +6,62 @@ using System.Collections; using System.Collections.Generic; +#nullable enable + namespace Microsoft.Data.SqlClient.Tests.Common { + /// + /// Utility class that enables disposal of a collection of objects + /// with a single using statement. + /// + /// Type of the elements contained within. public class DisposableArray : IDisposable, IEnumerable - where T : IDisposable + where T : IDisposable? { private readonly T[] _elements; - public T this[int i] - { - get => _elements[i]; - set => _elements[i] = value; - } - - public int Length => _elements.Length; - + /// + /// Constructs a new instance with elements. + /// + /// + /// Remember when using this constructor that the underlying array will be initialized to + /// default(T). If is a reference type, this will be + /// null - even if is not nullable! + /// + /// Number of elements the new instance will contain. public DisposableArray(int size) { _elements = new T[size]; } + /// + /// Constructs a new instance from an existing array of elements. + /// + /// Array of elements to store within the current instance. public DisposableArray(T[] elements) { _elements = elements; } + /// + /// Gets or sets the element at index . + /// + /// The element to get/set will be at this position in the array + public T this[int i] + { + get => _elements[i]; + set => _elements[i] = value; + } + + /// + /// Gets the number of elements in the array. + /// + public int Length => _elements.Length; + + /// + /// Disposes all elements in the current instance. Each element will be checked for + /// null before disposing it. + /// public void Dispose() { foreach (T element in _elements) @@ -41,9 +72,11 @@ public void Dispose() GC.SuppressFinalize(this); } + /// public IEnumerator GetEnumerator() => ((IEnumerable)_elements).GetEnumerator(); + /// IEnumerator IEnumerable.GetEnumerator() => _elements.GetEnumerator(); } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index ff9a704b9a..916b9ac04d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -409,9 +409,6 @@ public static bool AreConnStringsSetup() return !string.IsNullOrEmpty(NPConnectionString) && !string.IsNullOrEmpty(TCPConnectionString); } - public static bool AreConnStringsNotAzureSynapse() => - AreConnStringsSetup() && IsNotAzureSynapse(); - public static bool IsSQL2022() => string.Equals("16", SQLServerVersion.Trim()); public static bool IsSQL2019() => string.Equals("15", SQLServerVersion.Trim()); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs index 687ae1696e..06af375019 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs @@ -368,7 +368,7 @@ private static void AssertSessionsAndRequests( private static DisposableArray GetCommands(SqlConnection connection, CommandType commandType) { - SqlCommand[] result = new SqlCommand[ConcurrentCommands]; + DisposableArray result = new(ConcurrentCommands); for (int i = 0; i < result.Length; i++) { switch (commandType) @@ -401,7 +401,7 @@ private static DisposableArray GetCommands(SqlConnection connection, } } - return new DisposableArray(result); + return result; } private static SqlConnection GetConnection()