Open sandboxFocusImprove this doc

Retry example, step 3: Handling cancellation tokens

Our next goal, now that we have appropriately handled async methods, is to handle the CancellationToken parameters. When there is a cancellation token, we should transfer it to the call to Task.Delay.

Source Code




1internal class RemoteCalculator
2{
3    private static int _attempts;
4
5    [Retry( Attempts = 5 )]
6    public int Add( int a, int b )
7    {




8        // Let's pretend this method executes remotely
9        // and can fail for network reasons.
10
11        Thread.Sleep( 10 );
12
13        _attempts++;
14        Console.WriteLine( $"Trying for the {_attempts}-th time." );
15
16        if ( _attempts <= 3 )
17        {
18            throw new InvalidOperationException();
19        }
20
21        Console.WriteLine( $"Succeeded." );
22
23        return a + b;








24    }
25
26    [Retry( Attempts = 5 )]
27    public async Task<int> AddAsync( int a, int b, CancellationToken cancellationToken = default )
28    {
















29        // Let's pretend this method executes remotely
30        // and can fail for network reasons.
31
32        await Task.Delay( 10, cancellationToken );
33
34        _attempts++;
35        Console.WriteLine( $"Trying for the {_attempts}-th time." );
36
37        if ( _attempts <= 3 )
38        {
39            throw new InvalidOperationException();
40        }
41
42        Console.WriteLine( $"Succeeded." );
43
44        return a + b;
45    }
46}
Transformed Code
1using System;
2using System.Threading;
3using System.Threading.Tasks;
4
5internal class RemoteCalculator
6{
7    private static int _attempts;
8
9    [Retry( Attempts = 5 )]
10    public int Add( int a, int b )
11    {
12for (var i = 0; ; i++)
13        {
14            try
15            {
16                // Let's pretend this method executes remotely
17                // and can fail for network reasons.
18
19                Thread.Sleep( 10 );
20
21        _attempts++;
22        Console.WriteLine( $"Trying for the {_attempts}-th time." );
23
24        if ( _attempts <= 3 )
25        {
26            throw new InvalidOperationException();
27        }
28
29        Console.WriteLine( $"Succeeded." );
30
31        return a + b;
32}
33            catch (Exception e) when (i < 5)
34            {
35                var delay = 100 * Math.Pow(2, i + 1);
36                Console.WriteLine(e.Message + $" Waiting {delay} ms.");
37                Thread.Sleep((int)delay);
38            }
39        }
40    }
41
42    [Retry( Attempts = 5 )]
43    public async Task<int> AddAsync( int a, int b, CancellationToken cancellationToken = default )
44    {
45for (var i = 0; ; i++)
46        {
47            try
48            {
49                return (int)(await this.AddAsync_Source(a, b, cancellationToken));
50            }
51            catch (Exception e) when (i < 5)
52            {
53                var delay = 100 * Math.Pow(2, i + 1);
54                Console.WriteLine(e.Message + $" Waiting {delay} ms.");
55                await Task.Delay((int)delay, cancellationToken);
56            }
57        }
58    }
59private async Task<int> AddAsync_Source(int a, int b, CancellationToken cancellationToken = default)
60    {
61        // Let's pretend this method executes remotely
62        // and can fail for network reasons.
63
64        await Task.Delay( 10, cancellationToken );
65
66        _attempts++;
67        Console.WriteLine( $"Trying for the {_attempts}-th time." );
68
69        if ( _attempts <= 3 )
70        {
71            throw new InvalidOperationException();
72        }
73
74        Console.WriteLine( $"Succeeded." );
75
76        return a + b;
77    }
78}

Implementation

In this example, we have adjusted the OverrideMethodAspect template in the following way:

1using Metalama.Framework.Aspects;
2
3public class RetryAttribute : OverrideMethodAspect
4{
5    /// <summary>
6    /// Gets or sets the maximum number of times that the method should be executed.
7    /// </summary>
8    public int Attempts { get; set; } = 3;
9
10    /// <summary>
11    /// Gets or set the delay, in ms, to wait between the first and the second attempt.
12    /// The delay is doubled at every further attempt.
13    /// </summary>
14    public double Delay { get; set; } = 100;
15
16    // Template for non-async methods.
17    public override dynamic? OverrideMethod()
18    {
19        for ( var i = 0;; i++ )
20        {
21            try
22            {
23                return meta.Proceed();
24            }
25            catch ( Exception e ) when ( i < this.Attempts )
26            {
27                var delay = this.Delay * Math.Pow( 2, i + 1 );
28                Console.WriteLine( e.Message + $" Waiting {delay} ms." );
29                Thread.Sleep( (int) delay );
30            }
31        }
32    }
33
34    // Template for async methods.
35    public override async Task<dynamic?> OverrideAsyncMethod()
36    {
37        var cancellationTokenParameter
38            = meta.Target.Parameters.LastOrDefault( p => p.Type.Equals( typeof(CancellationToken) ) );
39
40        for ( var i = 0;; i++ )
41        {
42            try
43            {
44                return await meta.ProceedAsync();
45            }
46            catch ( Exception e ) when ( i < this.Attempts )
47            {
48                var delay = this.Delay * Math.Pow( 2, i + 1 );
49                Console.WriteLine( e.Message + $" Waiting {delay} ms." );
50
51                if ( cancellationTokenParameter != null )
52                {
53                    await Task.Delay( (int) delay, cancellationTokenParameter.Value );
54                }
55                else
56                {
57                    await Task.Delay( (int) delay );
58                }
59            }
60        }
61    }
62}

Let's look at the first lines:

var cancellationTokenParameter
= meta.Target.Parameters.Where( p => p.Type.Is( typeof( CancellationToken ) ) ).LastOrDefault();

This code defines a local variable and assigns it to the last parameter of type CancellationToken, or to null if there is no such parameter. The expression meta.Target.Parameters gives access to the parameters of the method to which the template is applied. This expression is evaluated at compile time and, by transitivity, the cancellationTokenParameter variable is also defined as a compile-time local variable.

We use this variable to determine which overload of Task.Delay to use. The if statement is executed at compile time, as the cancellationTokenParameter != null expression is entirely known at compile time. cancellationTokenParameter.Value returns the run-time value of the parameter, which translates to cancellationToken in most cases.

if ( cancellationTokenParameter != null )
{
      await Task.Delay( (int) delay, cancellationTokenParameter.Value );
}
else
{
      await Task.Delay( (int) delay );
}

Limitations

Our example still has two drawbacks:

  1. The logging is too fundamental and hardcoded to Console.WriteLine.