Skip to main content

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);
        }
    }
}