Calls to a database or a remote service can fail for reasons that go away on their own: a congested network, a brief outage, a lock timeout. Retrying the call, typically with exponential backoff, is often enough to recover from these transient faults.
This series of articles describes how to construct an aspect that automatically retries a failed method. This aspect modifies a method in the following way:
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 )
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}
Transformed Code
1using Microsoft.Extensions.Logging;
2
3internal class RemoteCalculator
4{
5 private static int _attempts;
6
7 [Retry( Attempts = 5 )]
8 public int Add( int a, int b )
9 {for (var i = 0; ; i++)
10 {
11 try
12 {
13 // Let's pretend this method executes remotely
14 // and can fail for network reasons.
15
16 Thread.Sleep( 10 );
17
18 _attempts++;
19 Console.WriteLine( $"Trying for the {_attempts}-th time." );
20
21 if ( _attempts <= 3 )
22 {
23 throw new InvalidOperationException();
24 }
25
26 Console.WriteLine( $"Succeeded." );
27
28 return a + b;
29}
30 catch (Exception e) when (i < 5)
31 {
32 var delay = 100 * Math.Pow(2, i + 1);
33 _logger.LogWarning($"RemoteCalculator.Add(a = {{{a}}}, b = {{{b}}}) has failed: {e.Message} Retrying in {delay} ms.");
34 Thread.Sleep((int)delay);
35 _logger.LogTrace($"RemoteCalculator.Add(a = {{{a}}}, b = {{{b}}}): retrying now.");
36 }
37 }
38 }
39 [Retry( Attempts = 5 )]
40 public async Task<int> AddAsync( int a, int b )
41 {for (var i = 0; ; i++)
42 {
43 try
44 {
45 // Let's pretend this method executes remotely
46 // and can fail for network reasons.
47
48 await Task.Delay( 10 );
49
50 _attempts++;
51 Console.WriteLine( $"Trying for the {_attempts}-th time." );
52
53 if ( _attempts <= 3 )
54 {
55 throw new InvalidOperationException();
56 }
57
58 Console.WriteLine( $"Succeeded." );
59
60 return a + b;
61}
62 catch (Exception e) when (i < 5)
63 {
64 var delay = 100 * Math.Pow(2, i + 1);
65 _logger.LogWarning($"RemoteCalculator.AddAsync(a = {{{a}}}, b = {{{b}}}) has failed: {e.Message} Retrying in {delay} ms.");
66 await Task.Delay((int)delay);
67 _logger.LogTrace($"RemoteCalculator.AddAsync(a = {{{a}}}, b = {{{b}}}): retrying now.");
68 }
69 }
70 }
71 private ILogger _logger;
72
73 public RemoteCalculator(ILogger<RemoteCalculator> logger = null)
74 {
75 this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
76 }
77}
In this series
We start with the most basic implementation and add features progressively.
| Description | Article |
|---|---|
| Retry example, step 1: Getting started | This is the most basic retry aspect. |
| Retry example, step 2: Handling async methods | In this example, we add support for async methods and call await Task.Delay instead of Thread.Sleep. |
| Retry example, step 3: Handling cancellation tokens | Here, we add support for CancellationToken parameters, which we pass to Task.Delay. |
| Retry example, step 4: Adding logging | We now add proper logging using ILogger and dependency injection. |
| Retry example, step 5: Using Polly | Finally, we show how to use Polly instead of our custom and naïve implementation of the retry logic. |