Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you catch CancellationToken.Register callback exceptions?

I am using async I/O to communicate with an HID device, and I would like to throw a catchable exception when there is a timeout. I've got the following read method:

public async Task<int> Read( byte[] buffer, int? size=null )
{
    size = size ?? buffer.Length;

    using( var cts = new CancellationTokenSource() )
    {
        cts.CancelAfter( 1000 );
        cts.Token.Register( () => { throw new TimeoutException( "read timeout" ); }, true );
        try
        {
            var t =  stream.ReadAsync( buffer, 0, size.Value, cts.Token );
            await t;
            return t.Result;
        }
        catch( Exception ex )
        {
            Debug.WriteLine( "exception" );
            return 0;
        }
    }
}

The exception thrown from the Token's callback is not caught by any try/catch blocks and I'm not sure why. I assumed it would be thrown at the await, but it is not. Is there a way to catch this exception (or make it catchable by the caller of Read())?

EDIT: So I re-read the doc at msdn, and it says "Any exception the delegate generates will be propagated out of this method call."

I'm not sure what it means by "propagated out of this method call", because even if I move the .Register() call into the try block the exception is still not caught.

like image 700
bj0 Avatar asked May 12 '14 20:05

bj0


2 Answers

Exception handling for callbacks registered with CancellationToken.Register() is complex. :-)

Token Cancelled Before Callback Registration

If the cancellation token is cancelled before the cancellation callback is registered, the callback will be executed synchronously by CancellationToken.Register(). If the callback raises an exception, that exception will be propagated from Register() and so can be caught using a try...catch around it.

This propagation is what the statement you quoted refers to. For context, here's the full paragraph that that quote comes from.

If this token is already in the canceled state, the delegate will be run immediately and synchronously. Any exception the delegate generates will be propagated out of this method call.

"This method call" refers to the call to CancellationToken.Register(). (Don't feel bad about being confused by this paragraph. When I first read it a while back, I was puzzled, too.)

Token Cancelled After Callback Registration

Cancelled by Calling CancellationTokenSource.Cancel()

When the token is cancelled by calling this method, cancellation callbacks are executed synchronously by it. Depending on the overload of Cancel() that's used, either:

  • All cancellation callbacks will be run. Any exceptions raised will be combined into an AggregateException that is propagated out of Cancel().
  • All cancellation callbacks will be run unless and until one throws an exception. If a callback throws an exception, that exception will be propagated out of Cancel() (not wrapped in an AggregateException) and any unexecuted cancellation callbacks will be skipped.

In either case, like CancellationToken.Register(), a normal try...catch can be used to catch the exception.

Cancelled by CancellationTokenSource.CancelAfter()

This method starts a countdown timer then returns. When the timer reaches zero, the timer causes the cancellation process to run in the background.

Since CancelAfter() doesn't actually run the cancellation process, cancellation callback exceptions aren't propagated out of it. If you want to observer them, you'll need to revert to using some means of intercepting unhandled exceptions.

In your situation, since you're using CancelAfter(), intercepting the unhandled exception is your only option. try...catch won't work.

Recommendation

To avoid these complexities, when possible don't allow cancellation callbacks to throw exceptions.

Further Reading

  • CancellationTokenSource.Cancel() - talks about how cancellation callback exceptions are handled
  • Understanding Cancellation Callbacks - a blog post I recently wrote on this topic
like image 127
Ben Gribaudo Avatar answered Oct 05 '22 20:10

Ben Gribaudo


I personally prefer to wrap the Cancellation logic into it's own method.

For example, given an extension method like:

public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<bool>();
    using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
    {
        if (task != await Task.WhenAny(task, tcs.Task))
        {
            throw new OperationCanceledException(cancellationToken);
        }
    }

    return task.Result;
}

You can simplify your method down to:

public async Task<int> Read( byte[] buffer, int? size=null )
{
    size = size ?? buffer.Length;

    using( var cts = new CancellationTokenSource() )
    {
        cts.CancelAfter( 1000 );
        try
        {
            return await stream.ReadAsync( buffer, 0, size.Value, cts.Token ).WithCancellation(cts.Token);
        }
        catch( OperationCanceledException cancel )
        {
            Debug.WriteLine( "cancelled" );
            return 0;
        }
        catch( Exception ex )
        {
            Debug.WriteLine( "exception" );
            return 0;
        }
    }
}

In this case, since your only goal is to perform a timeout, you can make this even simpler:

public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
{
    if (task != await Task.WhenAny(task, Task.Delay(timeout)))
    {
        throw new TimeoutException();
    }

    return task.Result; // Task is guaranteed completed (WhenAny), so this won't block
}

Then your method can be:

public async Task<int> Read( byte[] buffer, int? size=null )
{
    size = size ?? buffer.Length;

    try
    {
        return await stream.ReadAsync( buffer, 0, size.Value, cts.Token ).TimeoutAfter(TimeSpan.FromSeconds(1));
    }
    catch( TimeoutException timeout )
    {
        Debug.WriteLine( "Timed out" );
        return 0;
    }
    catch( Exception ex )
    {
        Debug.WriteLine( "exception" );
        return 0;
    }
}
like image 41
Reed Copsey Avatar answered Oct 05 '22 19:10

Reed Copsey