Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async.Catch doesnt work on OperationCanceledExceptions

I use Async.Catch to handle exceptions thrown by async workflows:

work
|> Async.Catch
|> Async.RunSynchronously
|> fun x -> match x with
            | Choice1Of2 _ -> () // success
            | Choice2Of2 ex -> // failure, handle exception

Today I noticed that OperationCanceledExceptions aren't handled by Async.Catch. Instead of getting a Choice from Async.Catch the exception keeps bubbling up until it hits me. I expected the following test to be red, but it's green:

  [<Test>]
  let ``Async.Catch doesnt work on OperationCancelledExceptions``() =
    use cancellationTokenSource = new System.Threading.CancellationTokenSource(1000)

    let work = async {
      while true do
        do! Async.Sleep 100
    }

    (fun () -> work
               |> Async.Catch
               |> fun x -> Async.RunSynchronously (x, cancellationToken=cancellationTokenSource.Token)
               |> ignore)
    |> should throw typeof<System.OperationCanceledException>

Evaluating some exceptions with Async.Catch + Choices + matching and some others using try/catch blocks doesn't seem right... it would look like the following, which is way too complicated. Besides that I wonder what use Async.Catch has, since I have to use a try/catch block anyway...:

  [<Test>]
  let ``evaluating exceptions of async workflows``() =
    use cancellationTokenSource = new System.Threading.CancellationTokenSource(1000)

    let work = async {
      while true do
        do! Async.Sleep 100
    }

    try
      work
      |> Async.Catch
      |> fun x -> Async.RunSynchronously (x, cancellationToken=cancellationTokenSource.Token)
      |> fun x -> match x with
                  | Choice1Of2 result -> () // success, process result
                  | Choice2Of2 ex -> () // failure, handle exception
    with ex -> () // another failure, handle exception here too

What's the best way to handle exceptions of async workflows? Should I just dump Async.Catch and use try/catch blocks everywhere?

like image 826
stmax Avatar asked Aug 16 '13 13:08

stmax


1 Answers

Cancellation is a special kind of exception in asynchronous computations. When a workflow is cancelled, this also cancels all child computations (the cancellation token is shared). So, if you could handle cancellation as an ordinary exception, it could still cancel some other parts of your computation (and it would be hard to reason about what is going on).

However, you can write a primitive that starts a workflow (and separates it from the parent workflow) and then handles cancellation in this sub-workflow.

type Async = 
  static member StartCatchCancellation(work, ?cancellationToken) = 
    Async.FromContinuations(fun (cont, econt, _) ->
      // When the child is cancelled, report OperationCancelled
      // as an ordinary exception to "error continuation" rather
      // than using "cancellation continuation"
      let ccont e = econt e
      // Start the workflow using a provided cancellation token
      Async.StartWithContinuations( work, cont, econt, ccont, 
                                    ?cancellationToken=cancellationToken) )

The usage is similar to Async.Catch, but you have to pass the cancellation token to StartCatchCancellation rather than passing it to the main RunSynchronously (because the workflow is started separately):

let work = 
  async { while true do
            do! Async.Sleep 100 }

let ct = new System.Threading.CancellationTokenSource(10000)
Async.StartCatchCancellation(work, ct.Token) 
|> Async.Catch
|> Async.RunSynchronously 
|> printfn "%A"
like image 98
Tomas Petricek Avatar answered Nov 03 '22 14:11

Tomas Petricek