I tried to reduce this to the smallest possible repro, but it's still a bit long-ish, my apologies.
I have an F# project that references a C# project with code like the following.
public static class CSharpClass {
public static async Task AsyncMethod(CancellationToken cancellationToken) {
await Task.Delay(3000);
cancellationToken.ThrowIfCancellationRequested();
}
}
Here's the F# code.
type Message =
| Work of CancellationToken
| Quit of AsyncReplyChannel<unit>
let mkAgent() = MailboxProcessor.Start <| fun inbox ->
let rec loop() = async {
let! msg = inbox.TryReceive(250)
match msg with
| Some (Work cancellationToken) ->
let! result =
CSharpClass.AsyncMethod(cancellationToken)
|> Async.AwaitTask
|> Async.Catch
// THIS POINT IS NEVER REACHED AFTER CANCELLATION
match result with
| Choice1Of2 _ -> printfn "Success"
| Choice2Of2 exn -> printfn "Error: %A" exn
return! loop()
| Some (Quit replyChannel) -> replyChannel.Reply()
| None -> return! loop()
}
loop()
[<EntryPoint>]
let main argv =
let agent = mkAgent()
use cts = new CancellationTokenSource()
agent.Post(Work cts.Token)
printfn "Press any to cancel."
System.Console.Read() |> ignore
cts.Cancel()
printfn "Cancelled."
agent.PostAndReply Quit
printfn "Done."
System.Console.Read()
The issue is that, upon cancellation, control never returns to the async block. I'm not sure if it's hanging in AwaitTask
or Catch
. Intuition tells me it's blocking when trying to return to the previous sync context, but I'm not sure how to confirm this. I'm looking for ideas on how to troubleshoot this, or perhaps someone with a deeper understanding here can spot the issue.
POSSIBLE SOLUTION
let! result =
Async.FromContinuations(fun (cont, econt, _) ->
let ccont e = econt e
let work = CSharpClass.AsyncMethod(cancellationToken) |> Async.AwaitTask
Async.StartWithContinuations(work, cont, econt, ccont))
|> Async.Catch
OperationCanceledException
has special handling in F#, always stops the whole async process, all the way up to the turtles. See this answer: https://mcmap.net/q/868820/-f-how-async-lt-39-t-gt-cancellation-works – ObscurantismOperationCanceledException
is handled. You can even throw it yourself, without involving a token, and it will still stop the whole async. – ObscurantismTaskCompletionSource<unit>
, which essentially returns a null result – AlidisContinueWith
would throw a NRE.Async.AwaitTask >> Async.Catch
handles that case. – Coimbra