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
.
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}
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:
- The logging is too fundamental and hardcoded to
Console.WriteLine
.