Skip to content

Async.StartAsTask behavior in cancellation scenarios. #3219

Closed
@matthid

Description

@matthid

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:

/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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions