diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index 184b5465ec..b2588cead5 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -45,6 +45,14 @@ public sealed class ApiShould : IClassFixture, IDis "'MSSQL_CERTIFICATE_STORE', 'MSSQL_CNG_STORE', 'MSSQL_CSP_PROVIDER'", $"'{NotRequiredProviderName}'"); + private HashSet _cancellationExceptionMessages = new HashSet() + { + "A severe error occurred on the current command. The results, if any, should be discarded.\r\nOperation cancelled by user.", + "A severe error occurred on the current command. The results, if any, should be discarded.", + "Operation cancelled by user.", + "The request failed to run because the batch is aborted, this can be caused by abort signal sent from client, or another request is running in the same session, which makes the session busy.\r\nOperation cancelled by user." + }; + public ApiShould(PlatformSpecificTestContext context) { _fixture = context.Fixture; @@ -1983,19 +1991,15 @@ public void TestBeginAndEndExecuteReaderWithAsyncCallback(string connection, Com } } - [SkipOnTargetFramework(TargetFrameworkMonikers.Netcoreapp)] [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE))] [ClassData(typeof(AEConnectionStringProviderWithExecutionMethod))] - public void TestSqlCommandCancel(string connection, string value, int number) + public void TestSqlCommandCancel(string connection, string value) { CleanUpTable(connection, _tableName); string executeMethod = value; Assert.True(!string.IsNullOrWhiteSpace(executeMethod), @"executeMethod should not be null or empty"); - int numberOfCancelCalls = number; - Assert.True(numberOfCancelCalls >= 0, "numberofCancelCalls should be >=0."); - IList values = GetValues(dataHint: 58); Assert.True(values != null && values.Count >= 3, @"values should not be null and count should be >= 3."); @@ -2007,7 +2011,10 @@ public void TestSqlCommandCancel(string connection, string value, int number) { sqlConnection.Open(); - using (SqlCommand sqlCommand = new SqlCommand($@"SELECT * FROM [{_tableName}] WHERE FirstName = @FirstName AND CustomerId = @CustomerId", + // WAITFOR DELAY is present to ensure that the command is always executing by the time the cancellation occurs. + // Without this, a command which has completed by the time the cancellation occurs will fail the test. It's last + // in the command to ensure that cancellation is tested while row data or metadata is flowing. + using (SqlCommand sqlCommand = new SqlCommand($@"SELECT * FROM [{_tableName}] WHERE FirstName = @FirstName AND CustomerId = @CustomerId; WAITFOR DELAY '00:00:00.150'", sqlConnection, transaction: null, columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) @@ -2017,29 +2024,50 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepDuringTryFetchInputParameterEncryptionInfo?.SetValue(null, true); - Thread[] threads = new Thread[2]; - - // Invoke ExecuteReader or ExecuteNonQuery in another thread. + Task[] tasks = new Task[2]; + ManualResetEventSlim startWorkloadSignal = new ManualResetEventSlim(false); + ManualResetEventSlim workloadCompleteSignal = new ManualResetEventSlim(false); + + /* + * Invoke ExecuteReader or ExecuteNonQuery in another thread. + * Use long-running tasks to create the thread. This enables any failed assertions to propagate, rather than + * allowing the exception to kill the thread and the process. + * These threads should progress in the sequence below: + * + * Workload Thread | Cancel Thread + * ------------------------------------ | ------------- + * Start thread | Start thread + * Wait for signal | - + * - | Set signal for workload start + * Start workload execution | Loop, cancelling workload until workload complete signal set + * Throw cancellation exception | - + * Set signal for workload complete | - + * End thread | Finish loop and end thread + */ if (executeMethod == @"ExecuteReader") { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteReader)); + tasks[0] = new Task(Thread_ExecuteReader, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } else { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteNonQuery)); + tasks[0] = new Task(Thread_ExecuteNonQuery, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } - - threads[1] = new Thread(new ParameterizedThreadStart(Thread_Cancel)); + tasks[1] = new Task(Thread_Cancel, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); // Start the execute thread. - threads[0].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[0].Start(); // Start the thread which cancels the above command started by the execute thread. - threads[1].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[1].Start(); // Wait for the threads to finish. - threads[0].Join(); - threads[1].Join(); + Task.WaitAll(tasks); CommandHelper.s_sleepDuringTryFetchInputParameterEncryptionInfo?.SetValue(null, false); @@ -2061,37 +2089,48 @@ public void TestSqlCommandCancel(string connection, string value, int number) Assert.True(rowsAffected == numberOfRows, "Unexpected number of rows affected as returned by EndExecuteReader."); - // Verify the state of the sql command object. + // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); + Assert.True(workloadCompleteSignal.IsSet); + startWorkloadSignal.Reset(); + workloadCompleteSignal.Reset(); CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, true); // Invoke ExecuteReader or ExecuteNonQuery in another thread. - threads = new Thread[2]; + tasks = new Task[2]; if (executeMethod == @"ExecuteReader") { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteReader)); + tasks[0] = new Task(Thread_ExecuteReader, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } else { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteNonQuery)); + tasks[0] = new Task(Thread_ExecuteNonQuery, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } - threads[1] = new Thread(new ParameterizedThreadStart(Thread_Cancel)); + tasks[1] = new Task(Thread_Cancel, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); // Start the execute thread. - threads[0].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[0].Start(); // Start the thread which cancels the above command started by the execute thread. - threads[1].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[1].Start(); // Wait for the threads to finish. - threads[0].Join(); - threads[1].Join(); + Task.WaitAll(tasks); CommandHelper.s_sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption?.SetValue(null, false); - // Verify the state of the sql command object. + // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); + Assert.True(workloadCompleteSignal.IsSet); + startWorkloadSignal.Reset(); + workloadCompleteSignal.Reset(); rowsAffected = 0; @@ -2113,31 +2152,37 @@ public void TestSqlCommandCancel(string connection, string value, int number) CommandHelper.s_sleepAfterReadDescribeEncryptionParameterResults?.SetValue(null, true); - threads = new Thread[2]; + tasks = new Task[2]; if (executeMethod == @"ExecuteReader") { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteReader)); + tasks[0] = new Task(Thread_ExecuteReader, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } else { - threads[0] = new Thread(new ParameterizedThreadStart(Thread_ExecuteNonQuery)); + tasks[0] = new Task(Thread_ExecuteNonQuery, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); } - threads[1] = new Thread(new ParameterizedThreadStart(Thread_Cancel)); + tasks[1] = new Task(Thread_Cancel, + new TestCommandCancelParams(sqlCommand, _tableName, startWorkloadSignal, workloadCompleteSignal), + TaskCreationOptions.LongRunning); // Start the execute thread. - threads[0].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[0].Start(); // Start the thread which cancels the above command started by the execute thread. - threads[1].Start(new TestCommandCancelParams(sqlCommand, _tableName, numberOfCancelCalls)); + tasks[1].Start(); // Wait for the threads to finish. - threads[0].Join(); - threads[1].Join(); + Task.WaitAll(tasks); CommandHelper.s_sleepAfterReadDescribeEncryptionParameterResults?.SetValue(null, false); - // Verify the state of the sql command object. + // Verify the state of the sql command object. Also ensure that the exit lock was set (and didn't time out.) VerifySqlCommandStateAfterCompletionOrCancel(sqlCommand); + Assert.True(workloadCompleteSignal.IsSet); rowsAffected = 0; @@ -3130,47 +3175,81 @@ private void TestCancellationToken(FieldInfo failpoint, SqlCommand sqlCommand, i Assert.True(rowsAffected == numberOfRows, "Unexpected number of rows affected as returned by EndExecuteReader."); } - private void Thread_ExecuteReader(object cancelCommandTestParamsObject) + private void Thread_ExecuteReader(object state) { + TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; + SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + SqlDataReader reader = null; + Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); - SqlCommand sqlCommand = ((TestCommandCancelParams)cancelCommandTestParamsObject).SqlCommand as SqlCommand; Assert.True(sqlCommand != null, "sqlCommand should not be null."); - string.Format(@"SELECT * FROM {0} WHERE FirstName = @FirstName AND CustomerId = @CustomerId", ((TestCommandCancelParams)cancelCommandTestParamsObject).TableName); - using (SqlDataReader reader = sqlCommand.ExecuteReader()) + try { - while (reader.Read()) + // Wait for the cancellation thread to open this lock... + cancelCommandTestParamsObject.StartWorkloadSignal.Wait(); + + Exception ex = Assert.ThrowsAny(() => { - Assert.Throws(() => sqlCommand.ExecuteReader()); - } + reader = sqlCommand.ExecuteReader(); + while (reader.Read()) + { } + }); + Assert.Contains(ex.Message, _cancellationExceptionMessages); + } + finally + { + reader?.Dispose(); + // ...and unlock the cancellation thread once we finish. + cancelCommandTestParamsObject.WorkloadCompleteSignal.Set(); } } - private void Thread_ExecuteNonQuery(object cancelCommandTestParamsObject) + private void Thread_ExecuteNonQuery(object state) { + TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; + SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); - SqlCommand sqlCommand = ((TestCommandCancelParams)cancelCommandTestParamsObject).SqlCommand as SqlCommand; Assert.True(sqlCommand != null, "sqlCommand should not be null."); - string.Format(@"UPDATE {0} SET FirstName = @FirstName WHERE FirstName = @FirstName AND CustomerId = @CustomerId", ((TestCommandCancelParams)cancelCommandTestParamsObject).TableName); + try + { + // Wait for the cancellation thread to open this lock... + cancelCommandTestParamsObject.StartWorkloadSignal.Wait(); - Exception ex = Assert.Throws(() => sqlCommand.ExecuteNonQuery()); - Assert.Equal(@"Operation cancelled by user.", ex.Message); + Exception ex = Assert.ThrowsAny(() => sqlCommand.ExecuteNonQuery()); + Assert.Contains(ex.Message, _cancellationExceptionMessages); + } + finally + { + // ...and unlock the cancellation thread once we finish. + cancelCommandTestParamsObject.WorkloadCompleteSignal.Set(); + } } - private void Thread_Cancel(object cancelCommandTestParamsObject) + private void Thread_Cancel(object state) { + TestCommandCancelParams cancelCommandTestParamsObject = state as TestCommandCancelParams; + SqlCommand sqlCommand = cancelCommandTestParamsObject?.SqlCommand; + int cancellations = 0; + System.Diagnostics.Stopwatch cancellationStart = new System.Diagnostics.Stopwatch(); + Assert.True(cancelCommandTestParamsObject != null, @"cancelCommandTestParamsObject should not be null."); - SqlCommand sqlCommand = ((TestCommandCancelParams)cancelCommandTestParamsObject).SqlCommand as SqlCommand; Assert.True(sqlCommand != null, "sqlCommand should not be null."); - Thread.Sleep(millisecondsTimeout: 500); + cancellationStart.Start(); + cancelCommandTestParamsObject.StartWorkloadSignal.Set(); - // Repeatedly cancel. - for (int i = 0; i < ((TestCommandCancelParams)cancelCommandTestParamsObject).NumberofTimesToRunCancel; i++) + // Repeatedly cancel until the other thread signals that it's completed execution + // (or until the command timeout has passed.) + do { sqlCommand.Cancel(); - } + cancellations++; + } while (!cancelCommandTestParamsObject.WorkloadCompleteSignal.Wait(0) + && cancellationStart.ElapsedMilliseconds <= sqlCommand.CommandTimeout); + cancellationStart.Stop(); } public void Dispose() @@ -3298,67 +3377,42 @@ internal TestAsyncCallBackStateObject(SqlCommand sqlCommand, int expectedRowsAff internal class TestCommandCancelParams { /// - /// SqlCommand object. + /// Return the SqlCommand object. /// - private readonly object _sqlCommand; + public SqlCommand SqlCommand { get; } /// - /// Name of the test/works as table name + /// Return the table name (usually equal to the name of the test.) /// - private readonly string _tableName; + public string TableName { get; } /// - /// number of times to run cancel. + /// Lock set by the thread cancelling the SqlCommand. /// - private readonly int _numberofCancelCommands; + public ManualResetEventSlim StartWorkloadSignal { get; } /// - /// Return the SqlCommand object. + /// Lock set by the thread executing the SqlCommand. /// - public object SqlCommand - { - get - { - return _sqlCommand; - } - } - - /// - /// Return the tablename. - /// - public object TableName - { - get - { - return _tableName; - } - } - - /// - /// Return the number of times to run cancel. - /// - public int NumberofTimesToRunCancel - { - get - { - return _numberofCancelCommands; - } - } + public ManualResetEventSlim WorkloadCompleteSignal { get; } /// /// Constructor. /// /// /// - /// - public TestCommandCancelParams(object sqlCommand, string tableName, int numberofTimesToCancel) + /// + /// + /// + public TestCommandCancelParams(SqlCommand sqlCommand, string tableName, ManualResetEventSlim startWorkloadSignal, ManualResetEventSlim workloadCompleteSignal) { Assert.True(sqlCommand != null, "sqlCommand should not be null."); Assert.True(!string.IsNullOrWhiteSpace(tableName), "tableName should not be null or empty."); - _sqlCommand = sqlCommand; - _tableName = tableName; - _numberofCancelCommands = numberofTimesToCancel; + SqlCommand = sqlCommand; + TableName = tableName; + StartWorkloadSignal = startWorkloadSignal; + WorkloadCompleteSignal = workloadCompleteSignal; } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs index 33d548cd85..9c85c4fc67 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/DatabaseHelper.cs @@ -290,10 +290,8 @@ public IEnumerator GetEnumerator() { foreach (string connStrAE in DataTestUtility.AEConnStrings) { - yield return new object[] { connStrAE, @"ExecuteReader", 1 }; - yield return new object[] { connStrAE, @"ExecuteReader", 3 }; - yield return new object[] { connStrAE, @"ExecuteNonQuery", 1 }; - yield return new object[] { connStrAE, @"ExecuteNonQuery", 3 }; + yield return new object[] { connStrAE, @"ExecuteReader" }; + yield return new object[] { connStrAE, @"ExecuteNonQuery" }; } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs index e9e29e5940..10b653967d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs @@ -171,7 +171,7 @@ public class ConnectionWorker : IDisposable private static List s_workerList = new(); private ManualResetEventSlim _doneEvent = new(false); private double _timeElapsed; - private Thread _thread; + private Task _task; private string _connectionString; private int _numOfTry; @@ -180,7 +180,7 @@ public ConnectionWorker(string connectionString, int numOfTry) s_workerList.Add(this); _connectionString = connectionString; _numOfTry = numOfTry; - _thread = new Thread(new ThreadStart(SqlConnectionOpen)); + _task = new Task(SqlConnectionOpen, TaskCreationOptions.LongRunning); } public static List WorkerList => s_workerList; @@ -191,7 +191,7 @@ public static void Start() { foreach (ConnectionWorker w in s_workerList) { - w._thread.Start(); + w._task.Start(); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs index 93101d5d38..460d566a6d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/MirroringTest/ConnectionOnMirroringTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Data; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -28,17 +29,14 @@ public static void TestMultipleConnectionToMirroredServer() builder.ConnectTimeout = 0; TestWorker worker = new TestWorker(builder.ConnectionString); - Thread childThread = new Thread(() => worker.TestMultipleConnection()); - childThread.Start(); + Task childTask = Task.Factory.StartNew(() => worker.TestMultipleConnection(), TaskCreationOptions.LongRunning); if (workerCompletedEvent.WaitOne(10000)) { - childThread.Join(); + childTask.Wait(); } else { - // currently Thread.Abort() throws PlatformNotSupportedException in CoreFx. - childThread.Interrupt(); throw new Exception("SqlConnection could not open and close successfully in timely manner. Possibly connection hangs."); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs index d7603ec807..bf99691907 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ParameterTest/ParametersTest.cs @@ -8,6 +8,7 @@ using System.Data; using System.Data.SqlTypes; using System.Threading; +using System.Threading.Tasks; using Xunit; using System.Globalization; @@ -957,30 +958,19 @@ private static void EnableOptimizedParameterBinding_ReturnSucceeds() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] public static void ClosedConnection_SqlParameterValueTest() { - var threads = new List(); - for (int i = 0; i < 100; i++) + var tasks = new Task[100]; + for (int i = 0; i < tasks.Length; i++) { - var t = new Thread(() => + var t = Task.Factory.StartNew(() => { for (int j = 0; j < 1000; j++) { - try - { - RunParameterTest(); - } - catch (Exception e) - { - Assert.Fail($"Unexpected exception occurred: {e.Message}"); - } + RunParameterTest(); } - }); - t.Start(); - threads.Add(t); - } - for (int i = 0; i < threads.Count; i++) - { - threads[i].Join(); + }, TaskCreationOptions.LongRunning); + tasks[i] = t; } + Task.WaitAll(tasks); } private static void RunParameterTest() @@ -998,7 +988,7 @@ private static void RunParameterTest() cm.Parameters.Add(new SqlParameter("@id2", SqlDbType.UniqueIdentifier) { Direction = ParameterDirection.Output }); try { - System.Threading.Tasks.Task task = cm.ExecuteNonQueryAsync(cancellationToken.Token); + Task task = cm.ExecuteNonQueryAsync(cancellationToken.Token); task.Wait(); } catch (Exception) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs index 86b5438a1a..79bf05a7f8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/RandomStressTest/RandomStressTest.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Text; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -70,8 +71,7 @@ public void TestMain() { for (int tcount = 0; tcount < ThreadCountDefault; tcount++) { - Thread t = new Thread(TestThread); - t.Start(); + _ = Task.Factory.StartNew(TestThread, TaskCreationOptions.LongRunning); } }