Description
Today I stumbled on some unexpected behavior when cancelling a asynchronous workflow which interacts with Tasks.
When StartAsTask
is used it is not giving any chance to the underlying workflow to cancel in a regular way and just continues with a OperationCanceledException
, basically throwing away all information from the workflow.
DISCLAIMER: Maybe this works as designed, but we should put a warning somewhere in that case :/
Repro steps
OK, to explain this see how different information can flow through the async-workflow:
let tcs = new TaskCompletionSource<_>()
let cts = new CancellationTokenSource()
use reg = cts.Token.Register(fun () -> tcs.SetException(Exception "Something bad happened"))
let a =
async {
cts.CancelAfter 500
do! tcs.Task |> Async.AwaitTask
} |> fun a -> Async.RunSynchronously(a, cancellationToken = cts.Token)
()
In this case you get (as imho expected) an AggregateException
with the "Something bad happened"
message.
Now use StartAsTask
which imho is just another way to start the same workflow:
let tcs = new TaskCompletionSource<_>()
let cts = new CancellationTokenSource()
use reg = cts.Token.Register(fun () -> tcs.SetException(Exception "Something bad happened"))
let a =
async {
cts.CancelAfter 500
do! tcs.Task |> Async.AwaitTask
} |> fun a -> Async.StartAsTask(a, cancellationToken = cts.Token)
a.Result
()
Expected behavior
Accessing .Result
throwing an AggregateException (possibly wrapping another AggregateException) wrapping the "Something bad happened"
exception.
Actual behavior
Message: System.AggregateException : One or more errors occurred.
----> System.Threading.Tasks.TaskCanceledException : A task was canceled.
Known workarounds
Do not use StartAsTask
. I copied the library implementation and added a timeout parameter (to give the underlying task some time to use the token and finish regulary).
Maybe the correct thing to do is to assume the workflow is finishing correctly when the token is forwarded?
Related information
Related discussions:
- Async.AwaitTask behaviour on task canceled
- Async.AwaitTask does not cancel on workflow cancellation
This one in particular is interesting because I'm suggesting the complete reverse (on a different API).
I'm suggesting we should "ignore" cancellation when interacting with tasks and probably add some other APIs to add timeouts or early cancellation on top. IMHO it's a better default than loosing information and having to write own implementations.
/cc @eiriktsarpalis
Note that you might argue that the "different information flow" only works because it was the last thing the workflow was waiting for, but this works as expected as well:
let tcs = new TaskCompletionSource<_>()
let cts = new CancellationTokenSource()
use reg = cts.Token.Register(fun () -> tcs.SetException(Exception "Something bad happened"))
let a =
async {
cts.CancelAfter 500
do! tcs.Task |> Async.AwaitTask
printfn "test"
return! async {
do! Async.Sleep 100
return 4 }
} |> fun a -> Async.RunSynchronously(a, cancellationToken = cts.Token)
()
(IE. the "Something bad happened" is returned)
- Operating system: Windows
- Branch: FSharp.Core Nuget package (4.1.17)
- .NET Runtime 4.5
- Visual Studio 2017
- Indications of severity: Not sure if this is a bug