I have a long running operation which I am putting on a background thread using TPL. What I have currently works but I am confused over where I should be handling my AggregateException during a cancellation request.
In a button click event I start my process:
private void button1_Click(object sender, EventArgs e)
{
Utils.ShowWaitCursor();
buttonCancel.Enabled = buttonCancel.Visible = true;
try
{
// Thread cancellation.
cancelSource = new CancellationTokenSource();
token = cancelSource.Token;
// Get the database names.
string strDbA = textBox1.Text;
string strDbB = textBox2.Text;
// Start duplication on seperate thread.
asyncDupSqlProcs =
new Task<bool>(state =>
UtilsDB.DuplicateSqlProcsFrom(token, mainForm.mainConnection, strDbA, strDbB),
"Duplicating SQL Proceedures");
asyncDupSqlProcs.Start();
//TaskScheduler uiThread = TaskScheduler.FromCurrentSynchronizationContext();
asyncDupSqlProcs.ContinueWith(task =>
{
switch (task.Status)
{
// Handle any exceptions to prevent UnobservedTaskException.
case TaskStatus.Faulted:
Utils.ShowDefaultCursor();
break;
case TaskStatus.RanToCompletion:
if (asyncDupSqlProcs.Result)
{
Utils.ShowDefaultCursor();
Utils.InfoMsg(String.Format(
"SQL stored procedures and functions successfully copied from '{0}' to '{1}'.",
strDbA, strDbB));
}
break;
case TaskStatus.Canceled:
Utils.ShowDefaultCursor();
Utils.InfoMsg("Copy cancelled at users request.");
break;
default:
Utils.ShowDefaultCursor();
break;
}
}, TaskScheduler.FromCurrentSynchronizationContext()); // Or uiThread.
return;
}
catch (Exception)
{
// Do stuff...
}
}
In the method DuplicateSqlProcsFrom(CancellationToken _token, SqlConnection masterConn, string _strDatabaseA, string _strDatabaseB, bool _bCopyStoredProcs = true, bool _bCopyFuncs = true) I have
DuplicateSqlProcsFrom(CancellationToken _token, SqlConnection masterConn, string _strDatabaseA, string _strDatabaseB, bool _bCopyStoredProcs = true, bool _bCopyFuncs = true)
{
try
{
for (int i = 0; i < someSmallInt; i++)
{
for (int j = 0; j < someBigInt; j++)
{
// Some cool stuff...
}
if (_token.IsCancellationRequested)
_token.ThrowIfCancellationRequested();
}
}
catch (AggregateException aggEx)
{
if (aggEx.InnerException is OperationCanceledException)
Utils.InfoMsg("Copy operation cancelled at users request.");
return false;
}
catch (OperationCanceledException)
{
Utils.InfoMsg("Copy operation cancelled at users request.");
return false;
}
}
In a button Click event (or using a delegate (buttonCancel.Click += delegate { /Cancel the Task/ }) I cancel theTask` as follows:
private void buttonCancel_Click(object sender, EventArgs e)
{
try
{
cancelSource.Cancel();
asyncDupSqlProcs.Wait();
}
catch (AggregateException aggEx)
{
if (aggEx.InnerException is OperationCanceledException)
Utils.InfoMsg("Copy cancelled at users request.");
}
}
This catches the OperationCanceledException fine in method DuplicateSqlProcsFrom and prints my message, but in the call-back provided by the asyncDupSqlProcs.ContinueWith(task => { ... }); above the task.Status is always RanToCompletion; it should be cancelled!
What is the right way to capture and deal with the Cancel() task in this case. I know how this is done in the simple cases shown in this example from the CodeProject and from the examples on MSDN but I am confused in this case when running a continuation.
How do I capture the cancel task in this case and how to ensure the task.Status is dealt with properly?
You’re catching the
OperationCanceledExceptionin yourDuplicateSqlProcsFrommethod, which prevents itsTaskfrom ever seeing it and accordingly setting its status toCanceled. Because the exception is handled,DuplicateSqlProcsFromfinishes without throwing any exceptions and its corresponding task finishes in theRanToCompletionstate.DuplicateSqlProcsFromshouldn’t be catching eitherOperationCanceledExceptionorAggregateException, unless it’s waiting on subtasks of its own. Any exceptions thrown (includingOperationCanceledException) should be left uncaught to propagate to the continuation task. In your continuation’sswitchstatement, you should be checkingtask.Exceptionin theFaultedcase and handlingCanceledin the appropriate case as well.In your continuation lambda,
task.Exceptionwill be anAggregateException, which has some handy methods for determining what the root cause of an error was, and handling it. Check the MSDN docs particularly for theInnerExceptions(note the “S”),GetBaseException,FlattenandHandlemembers.EDIT: on getting a
TaskStatusofFaultedinstead ofCanceled.On the line where you construct your
asyncDupSqlProcstask, use aTaskconstructor which accepts both yourDuplicateSqlProcsFromdelegate and theCancellationToken. That associates your token with the task.When you call
ThrowIfCancellationRequestedon the token inDuplicateSqlProcsFrom, theOperationCanceledExceptionthat is thrown contains a reference to the token that was cancelled. When the Task catches the exception, it compares that reference to theCancellationTokenassociated with it. If they match, then the task transitions toCanceled. If they don’t, the Task infrastructure has been written to assume that this is an unforeseen bug, and the task transitions toFaultedinstead.Task Cancellation in MSDN