Spaceman
Spaceman

Reputation: 1339

Dealing with multiple CancellationTokens with timeouts

I'm a little confused about how to implement cancellation tokens for the following case.

Say I have a method that has a cancellation token with no timeout specified like this.

public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
    try
    {
        Task.Delay(1000, cancellationToken)
    }
    catch (OperationCanceledException canceledException)
    {
        // Do something with canceledException
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
        throw;
    }
    catch (Exception exception)
    {
        // Do something with exception
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
        throw;
    }
}

But in that method I want to call another method that expects a CancellationToken except this time I want to set up a timeout on it, like this.

public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
    try
    {
        var innerCancellationTokenSource = new CancellationTokenSource();
        innerCancellationTokenSource.CancelAfter(1000);
        var innerCancellationToken = innerCancellationTokenSource.Token;

        await DoSomeElseAsyncThingAsync(innerCancellationToken);
    }
    catch (OperationCanceledException canceledException)
    {
        // Do something with canceledException
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
        throw;
    }
    catch (Exception exception)
    {
        // Do something with exception
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
        throw;
    }
}

How do I get the innerCancellationToken to respect the cancellation request from the cancellationToken parameter?

The best I can come up with is something like this:

public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
    try
    {
        await Task.WhenAny(
            DoSomeElseAsyncThingAsync(cancellationToken),
            KaboomAsync(100, cancellationToken)
        );
    }
    catch (OperationCanceledException canceledException)
    {
        // Do something with canceledException
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
        throw;
    }
    catch (Exception exception)
    {
        // Do something with exception
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
        throw;
    }
}

public static async Task KaboomAsync(int delay, CancellationToken cancellationToken = default)
{
    await Task.Delay(delay, cancellationToken);
    throw new OperationCanceledException();
}

But this isn't quite right; the KaboomAsync() function will always blow up, and this path seems gnarly. Is there a better pattern for this?


Post answer I created this static Util methods to save me putting that boilerplate in a million times.

Hopefully its useful to someone.

public static async Task<T> CancellableUnitOfWorkHelper<T>(
    Func<CancellationToken, Task<T>> unitOfWordFunc,
    int timeOut,
    CancellationToken cancellationToken = default
)
{
    try
    {
        var innerCancellationTokenSource = new CancellationTokenSource(timeOut);
        using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken))
            return await unitOfWordFunc(linkedTokenSource.Token);
    }
    catch (OperationCanceledException canceledException)
    {
        Console.WriteLine(
            cancellationToken.IsCancellationRequested
                ? "Manual or parent Timeout {0}"
                : "UnitOfWork Timeout {0}"
            , canceledException
        );

        throw;
    }
    catch (Exception exception)
    {
        Console.WriteLine("Exception {0}", exception);
        throw;
    }
}

public static async Task CancellableUnitOfWorkHelper(
    Func<CancellationToken, Task> unitOfWordFunc,
    int timeOut,
    CancellationToken cancellationToken = default
)
{
    try
    {
        var innerCancellationTokenSource = new CancellationTokenSource(timeOut);
        using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken))
            await unitOfWordFunc(linkedTokenSource.Token);
    }
    catch (OperationCanceledException canceledException)
    {
        Console.WriteLine(
            cancellationToken.IsCancellationRequested
                ? "Manual or parent Timeout {0}"
                : "UnitOfWork Timeout {0}"
            , canceledException
        );

        throw;
    }
    catch (Exception exception)
    {
        Console.WriteLine("Exception {0}", exception);
        throw;
    }
}

They can be used like this.

await Util.CancellableUnitOfWorkHelper(
   token => Task.Delay(1000, token),
   200
);

or

await Util.CancellableUnitOfWorkHelper(
   token => Task.Delay(1000, token),
   200,
   someExistingToken
);

In both examples it will timeout after 200 ms but the second will also respect manual cancels or timeouts from the "someExistingToken" token.

Upvotes: 2

Views: 3865

Answers (1)

Peter Duniho
Peter Duniho

Reputation: 70671

CancellationTokenSource has a method specifically for this scenario: CreateLinkedTokenSource

In your example, it might look something like this:

public static async Task DoSomeAsyncThingAsync(CancellationToken cancellationToken = default)
{
    try
    {
        var innerCancellationTokenSource = new CancellationTokenSource();

        using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(innerCancellationTokenSource.Token, cancellationToken))
        {
            innerCancellationTokenSource.CancelAfter(1000);

            await DoSomeElseAsyncThingAsync(linkedTokenSource.Token);
        }
    }
    catch (OperationCanceledException canceledException)
    {
        // Do something with canceledException
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", canceledException);
        throw;
    }
    catch (Exception exception)
    {
        // Do something with exception
        Console.WriteLine("DoSomeElseAsyncThingAsync {0}", exception);
        throw;
    }
}

Note that it's important to dispose the linked source, otherwise references from the parent token sources will prevent it from being garbage-collected.

See also Any way to differentiate Cancel and Timeout and When to dispose CancellationTokenSource?

Upvotes: 9

Related Questions