diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 499b22de44..e23e87e698 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -598,41 +598,41 @@ internal void Cancel(object caller) // Keep looping until we either grabbed the lock (and therefore sent attention) or the connection closes\breaks while ((!hasLock) && (_parser.State != TdsParserState.Closed) && (_parser.State != TdsParserState.Broken)) { + // try to take the lock so that if another command is attempted it will queue on the lock + // but don't require that the lock be taken because otherwise attention cannot be sent + // during command execution causing cancellation to wait + // This lock is also protecting against concurrent close and async continuations Monitor.TryEnter(this, _waitForCancellationLockPollTimeout, ref hasLock); - if (hasLock) - { // Lock for the time being - since we need to synchronize the attention send. - // This lock is also protecting against concurrent close and async continuations - // Ensure that, once we have the lock, that we are still the owner - if ((!_cancelled) && (_cancellationOwner.Target == caller)) - { - _cancelled = true; + // Ensure that that we are still the owner + if ((!_cancelled) && (_cancellationOwner.Target == caller)) + { + _cancelled = true; - if (_pendingData && !_attentionSent) + if (_pendingData && !_attentionSent) + { + bool hasParserLock = false; + // Keep looping until we have the parser lock (and so are allowed to write), or the connection closes\breaks + while ((!hasParserLock) && (_parser.State != TdsParserState.Closed) && (_parser.State != TdsParserState.Broken)) { - bool hasParserLock = false; - // Keep looping until we have the parser lock (and so are allowed to write), or the connection closes\breaks - while ((!hasParserLock) && (_parser.State != TdsParserState.Closed) && (_parser.State != TdsParserState.Broken)) + try { - try + _parser.Connection._parserLock.Wait(canReleaseFromAnyThread: false, timeout: _waitForCancellationLockPollTimeout, lockTaken: ref hasParserLock); + if (hasParserLock) { - _parser.Connection._parserLock.Wait(canReleaseFromAnyThread: false, timeout: _waitForCancellationLockPollTimeout, lockTaken: ref hasParserLock); - if (hasParserLock) - { - _parser.Connection.ThreadHasParserLockForClose = true; - SendAttention(); - } + _parser.Connection.ThreadHasParserLockForClose = true; + SendAttention(); } - finally + } + finally + { + if (hasParserLock) { - if (hasParserLock) + if (_parser.Connection.ThreadHasParserLockForClose) { - if (_parser.Connection.ThreadHasParserLockForClose) - { - _parser.Connection.ThreadHasParserLockForClose = false; - } - _parser.Connection._parserLock.Release(); + _parser.Connection.ThreadHasParserLockForClose = false; } + _parser.Connection._parserLock.Release(); } } } 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 7d8c233904..e7f213837b 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlCommand/SqlCommandCancelTest.cs @@ -282,5 +282,66 @@ private static void TimeOutDuringRead(string constr) throw; } } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void CancelDoesNotWait() + { + const int delaySeconds = 30; + const int cancelSeconds = 1; + + using (SqlConnection conn = new SqlConnection(s_connStr)) + using (var cmd = new SqlCommand($"WAITFOR DELAY '00:00:{delaySeconds:D2}'", conn)) + { + conn.Open(); + + Task.Delay(TimeSpan.FromSeconds(cancelSeconds)) + .ContinueWith(t => cmd.Cancel()); + + DateTime started = DateTime.UtcNow; + DateTime ended = DateTime.MinValue; + Exception exception = null; + try + { + cmd.ExecuteNonQuery(); + } + catch (Exception ex) + { + exception = ex; + } + ended = DateTime.UtcNow; + + Assert.NotNull(exception); + Assert.InRange((ended - started).TotalSeconds, 0, delaySeconds - 1); + } + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static async Task AsyncCancelDoesNotWait() + { + const int delaySeconds = 30; + const int cancelSeconds = 1; + + using (SqlConnection conn = new SqlConnection(s_connStr)) + using (var cmd = new SqlCommand($"WAITFOR DELAY '00:00:{delaySeconds:D2}'", conn)) + { + await conn.OpenAsync(); + + DateTime started = DateTime.UtcNow; + Exception exception = null; + try + { + await cmd.ExecuteNonQueryAsync(new CancellationTokenSource(TimeSpan.FromSeconds(cancelSeconds)).Token); + } + catch (Exception ex) + { + exception = ex; + } + DateTime ended = DateTime.UtcNow; + + Assert.NotNull(exception); + Assert.InRange((ended - started).TotalSeconds, cancelSeconds, delaySeconds - 1); + } + } + } }