/ Framework / Utils / Retry.cs
Retry.cs
  1  namespace Utils
  2  {
  3      public class Retry
  4      {
  5          private readonly string description;
  6          private readonly TimeSpan maxTimeout;
  7          private readonly TimeSpan sleepAfterFail;
  8          private readonly Action<Failure> onFail;
  9          private readonly bool failFast;
 10  
 11          public Retry(string description, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> onFail, bool failFast)
 12          {
 13              this.description = description;
 14              this.maxTimeout = maxTimeout;
 15              this.sleepAfterFail = sleepAfterFail;
 16              this.onFail = onFail;
 17              this.failFast = failFast;
 18          }
 19  
 20          public void Run(Action task)
 21          {
 22              var run = new RetryRun(description, task, maxTimeout, sleepAfterFail, onFail, failFast);
 23              run.Run();
 24          }
 25  
 26          public T Run<T>(Func<T> task)
 27          {
 28              T? result = default;
 29  
 30              var run = new RetryRun(description, () =>
 31              {
 32                  result = task();
 33              }, maxTimeout, sleepAfterFail, onFail, failFast);
 34              run.Run();
 35  
 36              return result!;
 37          }
 38  
 39          private class RetryRun
 40          {
 41              private readonly string description;
 42              private readonly Action task;
 43              private readonly TimeSpan maxTimeout;
 44              private readonly TimeSpan sleepAfterFail;
 45              private readonly Action<Failure> onFail;
 46              private readonly DateTime start = DateTime.UtcNow;
 47              private readonly List<Failure> failures = new List<Failure>();
 48              private readonly bool failFast;
 49              private int tryNumber;
 50              private DateTime tryStart;
 51  
 52              public RetryRun(string description, Action task, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> onFail, bool failFast)
 53              {
 54                  this.description = description;
 55                  this.task = task;
 56                  this.maxTimeout = maxTimeout;
 57                  this.sleepAfterFail = sleepAfterFail;
 58                  this.onFail = onFail;
 59                  this.failFast = failFast;
 60  
 61                  tryNumber = 0;
 62                  tryStart = DateTime.UtcNow;
 63              }
 64  
 65              public void Run()
 66              {
 67                  while (true)
 68                  {
 69                      CheckMaximums();
 70  
 71                      tryNumber++;
 72                      tryStart = DateTime.UtcNow;
 73                      try
 74                      {
 75                          task();
 76                          return;
 77                      }
 78                      catch (OperationCanceledException)
 79                      {
 80                          return;
 81                      }
 82                      catch (Exception ex)
 83                      {
 84                          var failure = CaptureFailure(ex);
 85                          onFail(failure);
 86                          Time.Sleep(sleepAfterFail);
 87                      }
 88                  }
 89              }
 90  
 91              private Failure CaptureFailure(Exception ex)
 92              {
 93                  var f = new Failure(ex, DateTime.UtcNow - tryStart, tryNumber);
 94                  failures.Add(f);
 95                  return f;
 96              }
 97  
 98              private void CheckMaximums()
 99              {
100                  if (Duration() > maxTimeout) Fail();
101                  if (tryNumber > 30) Fail();
102  
103                  // If we have a few very fast failures, retrying won't help us. There's probably something wrong with our operation.
104                  // In this case, don't wait the full duration and fail quickly.
105                  if (failFast && failures.Count > 5 && failures.All(f => f.Duration < TimeSpan.FromSeconds(1.0))) Fail();
106              }
107  
108              private void Fail()
109              {
110                  throw new TimeoutException($"Retry '{description}' timed out after {tryNumber} tries over {Time.FormatDuration(Duration())}: {GetFailureReport()}",
111                          new AggregateException(failures.Select(f => f.Exception)));
112              }
113  
114              private string GetFailureReport()
115              {
116                  return Environment.NewLine + string.Join(Environment.NewLine, failures.Select(f => f.Describe()));
117              }
118  
119              private TimeSpan Duration()
120              {
121                  return DateTime.UtcNow - start;
122              }
123          }
124      }
125  
126      public class Failure
127      {
128          public Failure(Exception exception, TimeSpan duration, int tryNumber)
129          {
130              Exception = exception;
131              Duration = duration;
132              TryNumber = tryNumber;
133          }
134  
135          public Exception Exception { get; }
136          public TimeSpan Duration { get; }
137          public int TryNumber { get; }
138  
139          public string Describe()
140          {
141              return $"Try {TryNumber} failed after {Time.FormatDuration(Duration)} with exception '{Exception}'";
142          }
143      }
144  }