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;
Canceled state. By throwing an OperationCanceledException and passing it the token on which cancellation was requested. The preferred way to perform is to use the ThrowIfCancellationRequested method.
Check the status of the query using the SP_who2 command. After some time, use the KILL command to KILL SPID using the following command. Execute this command in a new query window. Once we execute the KILL SPID command, SQL Server starts the ROLLBACK process for this query.
So CancellationToken can be used to terminate a request execution at the server immediately once the request is aborted or orphan. Here we are going to see some sample code snippets about implementing a CancellationToken for Entity FrameworkCore, Dapper ORM, and HttpClient calls in Asp. NetCore MVC application.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With