Implementing custom HTTP call retry with exponential backoff.
using System;
using System.Threading.Tasks;
namespace DotnetCommon.Utils
{
/// <summary>
/// Implementing custom HTTP call retries with exponential backoff.
/// </summary>
/// <example>
/// When working with cloud services and Docker containers, it's very important to always catch
/// TimeoutException, and retry the operation.
/// RetryWithExponentialBackoff makes it easy to implement such pattern.
/// Usage:
/// var retry = new RetryWithExponentialBackoff();
/// await retry.RunAsync(async ()=>
/// {
/// // work with HttpClient
/// });
/// </example>
/// <remarks>
/// Article: https://dzfweb.gitbooks.io/microsoft-microservices-book/content/implement-resilient-applications/implement-custom-http-call-retries-exponential-backoff.html
/// Gist: https://gist.github.com/CESARDELATORRE/6d7f647b29e55fdc219ee1fd2babb260
/// </remarks>
public sealed class RetryWithExponentialBackoff
{
private readonly int _maxRetries, _delayMilliseconds, _maxDelayMilliseconds;
public RetryWithExponentialBackoff(int maxRetries = 50, int delayMilliseconds = 200, int maxDelayMilliseconds = 2000)
{
_maxRetries = maxRetries;
_delayMilliseconds = delayMilliseconds;
_maxDelayMilliseconds = maxDelayMilliseconds;
}
public async Task RunAsync(Func<Task> func)
{
if (func == null)
{
throw new ArgumentNullException(nameof(func));
}
var backoff = new ExponentialBackoff(_maxRetries, _delayMilliseconds, _maxDelayMilliseconds);
retry:
try
{
await func()
.ConfigureAwait(false);
}
catch (Exception ex) when (ex is TimeoutException || ex is System.Net.Http.HttpRequestException)
{
//Debug.WriteLine("Exception raised is: " + ex.GetType().ToString() + " -- Message: " + ex.Message + " -- Inner Message: " + ex.InnerException.Message);
await backoff.Delay()
.ConfigureAwait(false);
goto retry;
}
}
}
/// <example>
/// ExponentialBackoff backoff = new ExponentialBackoff(3, 10, 100);
/// retry:
/// try {
/// // ...
/// }
/// catch (Exception ex) {
/// await backoff.Delay(cancellationToken);
/// goto retry;
/// }
/// </example>
/// <remarks>
/// Article: https://dzfweb.gitbooks.io/microsoft-microservices-book/content/implement-resilient-applications/implement-custom-http-call-retries-exponential-backoff.html
/// Gist: https://gist.github.com/CESARDELATORRE/6d7f647b29e55fdc219ee1fd2babb260
/// </remarks>
internal struct ExponentialBackoff
{
private readonly int _maxRetries, _delayMilliseconds, _maxDelayMilliseconds;
private int _retries, _pow;
public ExponentialBackoff(int maxRetries, int delayMilliseconds, int maxDelayMilliseconds)
{
_maxRetries = maxRetries;
_delayMilliseconds = delayMilliseconds;
_maxDelayMilliseconds = maxDelayMilliseconds;
_retries = 0;
_pow = 1;
}
public Task Delay()
{
if (_retries == _maxRetries)
{
throw new TimeoutException("Maximum retry attempts exceeded.");
}
++_retries;
if (_retries < 31)
{
_pow <<= 1; // _pow = Pow(2, _retries - 1)
}
var delay = Math.Min(_delayMilliseconds * (_pow - 1) / 2, _maxDelayMilliseconds);
return Task.Delay(delay);
}
}
}