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..a5ca8e639a --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/Common/DisposableArray.cs @@ -0,0 +1,83 @@ +// 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; + +#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? + { + private readonly T[] _elements; + + /// + /// 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) + { + element?.Dispose(); + } + + GC.SuppressFinalize(this); + } + + /// + public IEnumerator GetEnumerator() => + ((IEnumerable)_elements).GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => + _elements.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 733b483e75..bfa2080260 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 @@ -181,7 +181,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 c32bbeb668..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MARSSessionPoolingTest.cs +++ /dev/null @@ -1,276 +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.Runtime.CompilerServices; -using Xunit; - -namespace Microsoft.Data.SqlClient.ManualTesting.Tests -{ - public static 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_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) - { - PacketSize = 512, - MaxPoolSize = 1, - MultipleActiveResultSets = true - }).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); - } - - // 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); - } - - // 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); - } - - // 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() - { - 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); - } - - // 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() - { - 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); - - 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); - } - - 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(); - - 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.True(connections == (3 + i), "Failure - incorrect number of connections for NoCloses: " + connections + - "\ni: " + i + " :::: requests: " + requests + " :::: spid1: " + spid1 + " ::::: spid2: " + spid2); - - // 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); - break; - } - break; - } - } - } - } - } - } - - 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..06af375019 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MARSSessionPoolingTest/MarsSessionPoolingTest.cs @@ -0,0 +1,463 @@ +// 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.Threading; +using Microsoft.Data.SqlClient.Tests.Common; +using Xunit; + +#nullable enable + +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.ExecuteNonQuery(); + + // - 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) + { + 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 + int? observedSessions = null; + int expectedSessions = openMarsSessions + 2; + + // 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; + + // 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++) + { + (observedSessions, observedRequests) = QuerySessionCounters(connection); + if (observedSessions == expectedSessions && observedRequests == expectedRequests) + { + // We observed the expected values. + return; + } + + // Back off and wait before trying again + Thread.SpinWait(20 << attempt); + } + + // 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) + { + DisposableArray result = new(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 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; + } + + 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); + } + } +} 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..9f20521f08 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,15 @@ 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")] + [ActiveIssue("https://github.com/dotnet/SqlClient/issues/3755")] [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 +178,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); } @@ -348,26 +346,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"; + // Arrange + using SqlConnection connection = new SqlConnection(constr); + connection.Open(); - string errorMessage = SystemDataResourceManager.Instance.SQL_Timeout_Execution; - DataTestUtility.ExpectFailure(() => ExecuteReaderOnCmd(cmd), new string[] { errorMessage }); + 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; - VerifyConnection(cmd); - } - } - } + // Act + Action action = () => { using SqlDataReader reader = command.ExecuteReader(); }; - private static void ExecuteReaderOnCmd(SqlCommand cmd) - { - using (SqlDataReader reader = cmd.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 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..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,21 +40,20 @@ public static void CreateSqlConnectionWithCredential() } finally { - dropTestUser(user); + DropTestUser(user); } } - [ActiveIssue("9196")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] 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; @@ -74,21 +73,20 @@ public static void SqlConnectionChangePasswordPlaintext() } finally { - dropTestUser(user); + DropTestUser(user); } } - [ActiveIssue("9196")] [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] 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"); @@ -115,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"); @@ -178,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;" @@ -196,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;"