Canceling SQL Server query with CancellationToken
async-await, c#, cancellationtokensource, sql-server
Solution
After looking at what your stored procedure is doing, it appears that it is somehow blocking the cancellation.
If you change
RAISERROR (@msg,0,1) WITH NOWAIT;
to remove the `WITH NOWAIT` clause, then the cancellation works as expected. However, this prevents the `InfoMessage` events from firing in real time.
You could track progress of the long running stored procedure some other way or register for the token cancellation and call `cmd.Cancel()` since you know that works.
One other thing to note, with .NET 4.5, you can just use `Task.Run` instead of instantiating a `TaskFactory`.
So here's a working solution:
private CancellationTokenSource cts;
private async void TestSqlServerCancelSprocExecution()
{
cts = new CancellationTokenSource();
try
{
await Task.Run(() =>
{
using (SqlConnection conn = new SqlConnection("connStr"))
{
conn.InfoMessage += conn_InfoMessage;
conn.FireInfoMessageEventOnUserErrors = true;
conn.Open();
var cmd = conn.CreateCommand();
cts.Token.Register(() => cmd.Cancel());
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandText = "dbo.[CancelSprocTest]";
cmd.ExecuteNonQuery();
}
});
}
catch (SqlException)
{
// sproc was cancelled
}
}
private void cancelButton_Click(object sender, EventArgs e)
{
cts.Cancel();
}
In my testing of this, I had to wrap `ExecuteNonQuery` in a `Task` in order for `cmd.Cancel()` to work. If I used `ExecuteNonQueryAsync`, even without passing it a token, then the system would block on `cmd.Cancel()`. I'm not sure why that's the case, but wrapping the synchronous method in a Task provides a similar usage.
Problem
I have a long-running stored procedure in SQL Server that my users need to be able to cancel. I have written a small test app as follows that demonstrates that the `SqlCommand.Cancel()` method works quite nicely: ``` private SqlCommand cmd; private void TestSqlServerCancelSprocExecution() { TaskFactory f = new TaskFactory(); f.StartNew(() => { using (SqlConnection conn = new SqlConnection("connStr")) { conn.InfoMessage += conn_InfoMessage; conn.FireInfoMessageEventOnUserErrors = true; conn.Open(); cmd = conn.CreateCommand(); cmd.CommandType = CommandType.StoredProcedure; cmd.CommandText = "dbo.[CancelSprocTest]"; cmd.ExecuteNonQuery(); } }); } private void cancelButton_Click(object sender, EventArgs e) { if (cmd != null) { cmd.Cancel(); } } ``` Upon calling `cmd.Cancel()`, I can verify that the underlying stored procedure stops executing essentially immediately. Given that I use the async/await pattern quite heavily in my application, I was hoping that the async methods on `SqlCommand` that take `CancellationToken` parameters would work equally well. Unfortunately, I found that calling `Cancel()` on the `CancellationToken` caused the `InfoMessage` event handler to no longer be called, but the underlying stored procedure continued to execute. My test code for the async version follows: ``` private SqlCommand cmd; private CancellationTokenSource cts; private async void TestSqlServerCancelSprocExecution() { cts = new CancellationTokenSource(); using (SqlConnection conn = new SqlConnection("connStr")) { conn.InfoMessage += conn_InfoMessage; conn.FireInfoMessageEventOnUserErrors = true; conn.Open(); cmd = conn.CreateCommand(); cmd.CommandType = CommandType.StoredProcedure; cmd.CommandText = "dbo.[CancelSprocTest]"; await cmd.ExecuteNonQueryAsync(cts.Token); } } private void cancelButton_Click(object sender, EventArgs e) { cts.Cancel(); } ``` Am I missing something in how the `CancellationToken` is supposed to work? I'm on .NET 4.5.1 and SQL Server 2012 in case it matters. EDIT: I rewrote the test app as a console app in case the synchronization context was a factor and I see the same behavior -- the invocation of `CancellationTokenSource.Cancel()` does not stop the execution of the underlying stored procedure. EDIT: Here's the body of the stored procedure I'm calling in case that matters. It inserts records and prints results at one-second intervals to make it easy to see whether cancellation attempts took effect promptly. ``` WHILE (@loop <= 40) BEGIN DECLARE @msg AS VARCHAR(80) = 'Iteration ' + CONVERT(VARCHAR(15), @loop); RAISERROR (@msg,0,1) WITH NOWAIT; INSERT INTO foo VALUES (@loop); WAITFOR DELAY '00:00:01.01'; SET @loop = @loop+1; END; ```