Retry example, step 2: Handling async methods
In the previous example, async
methods were handled using the same template as normal methods. Consequently, we used a synchronous call to Thread.Sleep
instead of an asynchronous await Task.Delay
, which essentially negated the async
nature of the original method.
This new aspect addresses this problem, providing a template specifically for async
methods.
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 )
28 {
29 // Let's pretend this method executes remotely
30 // and can fail for network reasons.
31
32 await Task.Delay( 10 );
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;
4using Microsoft.Extensions.Logging;
5
6internal class RemoteCalculator
7{
8 private static int _attempts;
9
10 [Retry( Attempts = 5 )]
11 public int Add( int a, int b )
12 {
13 for (var i = 0; ; i++)
14 {
15 try
16 {
17 // Let's pretend this method executes remotely
18 // and can fail for network reasons.
19
20 Thread.Sleep( 10 );
21
22 _attempts++;
23 Console.WriteLine( $"Trying for the {_attempts}-th time." );
24
25 if ( _attempts <= 3 )
26 {
27 throw new InvalidOperationException();
28 }
29
30 Console.WriteLine( $"Succeeded." );
31
32 return a + b;
33 }
34 catch (Exception e) when (i < 5)
35 {
36 var delay = 100 * Math.Pow(2, i + 1);
37 Console.WriteLine(e.Message + $" Waiting {delay} ms.");
38 Thread.Sleep((int)delay);
39 }
40 }
41 }
42
43 [Retry( Attempts = 5 )]
44 public async Task<int> AddAsync( int a, int b )
45 {
46 for (var i = 0; ; i++)
47 {
48 try
49 {
50 return await this.AddAsync_Source(a, b);
51 }
52 catch (Exception e) when (i < 5)
53 {
54 var delay = 100 * Math.Pow(2, i + 1);
55 Console.WriteLine(e.Message + $" Waiting {delay} ms.");
56 await Task.Delay((int)delay);
57 }
58 }
59 }
60
61 private async Task<int> AddAsync_Source(int a, int b)
62 {
63 // Let's pretend this method executes remotely
64 // and can fail for network reasons.
65
66 await Task.Delay( 10 );
67
68 _attempts++;
69 Console.WriteLine( $"Trying for the {_attempts}-th time." );
70
71 if ( _attempts <= 3 )
72 {
73 throw new InvalidOperationException();
74 }
75
76 Console.WriteLine( $"Succeeded." );
77
78 return a + b;
79 }
80
81 private ILogger _logger;
82
83 public RemoteCalculator
84 (ILogger<RemoteCalculator> logger = default)
85 {
86 this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
87 }
88}
Implementation
The aspect provides a second template, OverrideAsyncMethod(), which provides the async
implementation of the method.
1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3using Microsoft.Extensions.Logging;
4
5public class RetryAttribute : OverrideMethodAspect
6{
7 [IntroduceDependency]
8 private readonly ILogger _logger;
9
10 /// <summary>
11 /// Gets or sets the maximum number of times that the method should be executed.
12 /// </summary>
13 public int Attempts { get; set; } = 3;
14
15 /// <summary>
16 /// Gets or set the delay, in ms, to wait between the first and the second attempt.
17 /// The delay is doubled at every further attempt.
18 /// </summary>
19 public double Delay { get; set; } = 100;
20
21 // Template for non-async methods.
22 public override dynamic? OverrideMethod()
23 {
24 for ( var i = 0;; i++ )
25 {
26 try
27 {
28 return meta.Proceed();
29 }
30 catch ( Exception e ) when ( i < this.Attempts )
31 {
32 var delay = this.Delay * Math.Pow( 2, i + 1 );
33 Console.WriteLine( e.Message + $" Waiting {delay} ms." );
34 Thread.Sleep( (int) delay );
35 }
36 }
37 }
38
39 // Template for async methods.
40 public override async Task<dynamic?> OverrideAsyncMethod()
41 {
42 for ( var i = 0;; i++ )
43 {
44 try
45 {
46 return await meta.ProceedAsync();
47 }
48 catch ( Exception e ) when ( i < this.Attempts )
49 {
50 var delay = this.Delay * Math.Pow( 2, i + 1 );
51 Console.WriteLine( e.Message + $" Waiting {delay} ms." );
52
53 await Task.Delay( (int) delay );
54 }
55 }
56 }
57}
The async
template uses await meta.ProceedAsync()
instead of meta.Proceed()
, and await Task.Delay
instead of Thread.Sleep
.
Limitations
There are still two limitations in this example:
- The aspect does not correctly handle a
CancellationToken
. - The logging is very basic and is hardcoded to
Console.WriteLine
.