In the following code:
if (await Task.WhenAny(task, Task.Delay(100)) == task) {
success = true;
}
else {
details += "Timed out on SendGrid.";
await task.ContinueWith(s =>
{
LogError(s.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
}
I occasionally get A task was cancelled
at the call to await task.ContinueWith
. My goal here is to see if the task
completes within 100ms - if it doesn't, I want to handle some logging (this particular task has a resource leak so I'm trying to work around it by wrapping it in a timeout). This is being pulled from the guidance here: Debugging Task.WhenAny and Push Notifications
Why is this happening, and what can I do to prevent this exception from being thrown?
This happens when your task
was not able to complete in time (in 100ms) BUT was able to complete later. You run your continuation with TaskContinuationOptions.OnlyOnFaulted
and such tasks got cancelled if original task did NOT faulted. You await
result of ContinueWith
so if your task is not faulted - your continuation is cancelled and you have an exception.
In general such way to handle timeout does not make much sense, because even after timeout is reached you still wait until original task is completed, to log an exception. I think your have to remove await
before your continuation. Then code will continue on timeout, but if task will fail later - it will be recorded.
There is another issue in your code - Task.WhenAny
will never throw. So this condition:
await Task.WhenAny(task, Task.Delay(100)) == task
does NOT mean success, because task
might be faulted. Always check task.Status
and task.Exception
even if WhenAny
indicates completion of your task. By the way, that answer you linked in your question mentions this.
Update: if you don't like VS warning about not awaited call - you can either disable it for this specific line, or use extension method like this:
static class TaskExtensions
{
public static void Forget(this Task task)
{
// do nothing
}
}
And then:
task.ContinueWith(s => {
Logger.Write(s.Exception);
}, TaskContinuationOptions.OnlyOnFaulted).Forget();
There is no harm in doing this (in this particular case of course), VS just issues this warning on every potentially awaitable call which is not awaited.
Task continuations are cancelled if they do not run. Your task continuation will run OnlyOnFaulted. If it doesn't fault, then the continuation will get cancelled.
Your two choices that don't involve handling that specific exception are to either not await anything (and not know if the Task has finished running or not), or to await the original Task. Once the await has finished, the task continuation will either run or get cancelled at that point.
If you're fine with handling the cancellation, then simply catch the TaskCancelledException (or AggregateException as appropriate) on the task continuation as needed and discard the exception.
The best way, in my opinion, for this specific case (logging the exception) is to just await the original task again and handle the TaskCancelledException, no task continuation needed and fully async/await compliant.
As already mentioned by @Evk it's your Continuation
that is cancelled. But I want to add that the continuation can be considered a piece of configuration and as such be set at the time the Task
is produced. Consider the following:
var task = Task.Delay(500).ContinueWith(s =>
{
LogError(s.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
if (await Task.WhenAny(task, Task.Delay(100)) == task) {
success = true;
} else {
details += "Timed out on SendGrid.";
}
In this approach your exception is still logged and your logic will carry on only knowing that the Task
timed out. If the rest of the logic does require knowledge of an Exception
in addition to a timeout then you will need to await
Task
again as already discussed.
Update for clarity
Original Code (1) At this stage we cannot see where task
is defined.
//task is undeclared in this snippet
if (await Task.WhenAny(task, Task.Delay(100)) == task) {
success = true;
}
else {
details += "Timed out on SendGrid.";
await task.ContinueWith(s =>
{
LogError(s.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
}
(2) Let's add a mock task just for example
var task = Task.Delay(500); //defines task as a Task that will complete in 500ms
if (await Task.WhenAny(task, Task.Delay(100)) == task) {
success = true;
}
else {
details += "Timed out on SendGrid.";
await task.ContinueWith(s =>
{
LogError(s.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
}
(3) Next, when we await task.ContinueWith
we allow a TaskCancelledException
to be thrown. If the await
is removed the Exception
can be ignored but we get a warning for a task that is not awaited. We can ignore the warning or we can recognize that the continuation
can be viewed as part of the configuration of a particular Task
. With that we can configure the Continuation
where we create the task with the appropriate options, ie TaskContinuationOptions.OnlyOnFaulted
:
//Add continuation configuration where task is created
var task = Task.Delay(500).ContinueWith(s =>
{
LogError(s.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
if (await Task.WhenAny(task, Task.Delay(100)) == task) {
success = true;
} else {
details += "Timed out on SendGrid.";
//removed continuation from here.
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.