When calling Async.RunSynchronously with a timeout and a CancellationToken, the timeout value seems to be ignored. I can work around this by calling CancelAfter on the CancellationToken, but ideally I'd like to be able to distinguish between exceptions that occur in the workflow, TimeOutExceptions and OperationCanceledExceptions.
I believe the sample code below demonstrates this.
open System
open System.Threading
let work = 
    async {
        let endTime = DateTime.UtcNow.AddMilliseconds(100.0)
        while DateTime.UtcNow < endTime do
            do! Async.Sleep(10)
            Console.WriteLine "working..."
        raise ( Exception "worked for more than 100 millis" )
    }
[<EntryPoint>]
let main argv = 
    try
        Async.RunSynchronously(work, 50)
    with
        | e -> Console.WriteLine (e.GetType().Name + ": " + e.Message)
    let cts = new CancellationTokenSource()
    try
        Async.RunSynchronously(work, 50, cts.Token)
    with
        | e -> Console.WriteLine (e.GetType().Name + ": " + e.Message)  
    cts.CancelAfter(80)
    try
        Async.RunSynchronously(work, 50, cts.Token)
    with
        | e -> Console.WriteLine (e.GetType().Name + ": " + e.Message)  
    Console.ReadKey(true) |> ignore
    0
The outputs the following, showing that the timeout is only effective in the first case (where no CancelationToken is specified)
working...
working...
TimeoutException: The operation has timed out.
working...
working...
working...
working...
working...
working...
working...
Exception: worked for more than 100 millis
working...
working...
working...
working...
working...
working...
OperationCanceledException: The operation was canceled.
Is this the intended behaviour? Is there any way get the behaviour I'm after?
Thanks!
                        
I'm not sure if this is intended behaviour - at least, I do not see any reason why it would be. However, this behaviour is implemented directly in the handling of parameters of
RunSynchronously. If you look at the library source code, you can see:In your case (with both timeout and a cancellation token that can be cancelled), the code goes through the last branch and ignores the timeout. I think this is either a bug or it is something that should be mentioned in the documentation.
As a workaround, you can create a separate
CancellationTokenSourceto specify the timeout and link it to the main cancellation source so that the caller provides (usingCreateLinkedTokenSource). When you getOperationCancelledException, you can then detect whether the source was an actual cancellation or a timeout: