Skip to main content

A utility class that executes both synchronous and asynchronous actions with retry logic, using exponential backoff and jitter.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public static class RetryPolicy
{
    private static readonly Random _random = new Random();

    /// <summary>
    /// Executes a synchronous action with retry logic, using exponential backoff and jitter.
    /// </summary>
    /// <param name="action">The action to execute.</param>
    /// <param name="retryCount">The number of retry attempts (default is 3).</param>
    /// <param name="baseDelayMs">The base delay in milliseconds (default is 500).</param>
    /// <example>
    /// <![CDATA[
    /// void UnreliableOperation()
    /// {
    ///     if (new Random().Next(2) == 0)
    ///         throw new Exception("Random failure!");
    ///
    ///     Console.WriteLine("Operation succeeded.");
    /// }
    ///
    /// RetryPolicy.Execute(UnreliableOperation, retryCount: 5, baseDelayMs: 200);
    /// ]]>
    /// </example>
    public static void Execute(Action action, int retryCount = 3, int baseDelayMs = 500)
    {
        for (int attempt = 0; attempt < retryCount; attempt++)
        {
            try
            {
                action();
                return;
            }
            catch
            {
                if (attempt == retryCount - 1) throw;

                int maxDelay = baseDelayMs * (int)Math.Pow(2, attempt);
                int jitter = _random.Next(0, maxDelay + 1);

                Thread.Sleep(jitter);
            }
        }
    }

    /// <summary>
    /// Executes a synchronous function with retry logic and returns a result, using exponential backoff and jitter.
    /// </summary>
    /// <typeparam name="T">Return type of the function.</typeparam>
    /// <param name="func">The function to execute.</param>
    /// <param name="retryCount">The number of retry attempts (default is 3).</param>
    /// <param name="baseDelayMs">The base delay in milliseconds (default is 500).</param>
    /// <returns>The result of the function.</returns>
    /// <example>
    /// <![CDATA[
    /// int SometimesFails()
    /// {
    ///     if (new Random().Next(3) != 0)
    ///         throw new Exception("Try again!");
    ///
    ///     return 42;
    /// }
    ///
    /// int result = RetryPolicy.Execute(SometimesFails);
    /// Console.WriteLine(result);
    /// ]]>
    /// </example>
    public static T Execute<T>(Func<T> func, int retryCount = 3, int baseDelayMs = 500)
    {
        for (int attempt = 0; attempt < retryCount; attempt++)
        {
            try
            {
                return func();
            }
            catch
            {
                if (attempt == retryCount - 1) throw;

                int maxDelay = baseDelayMs * (int)Math.Pow(2, attempt);
                int jitter = _random.Next(0, maxDelay + 1);

                Thread.Sleep(jitter);
            }
        }

        throw new Exception("Retry failed.");
    }

    /// <summary>
    /// Executes an asynchronous action with retry logic, using exponential backoff and jitter.
    /// </summary>
    /// <param name="action">The asynchronous action to execute.</param>
    /// <param name="retryCount">The number of retry attempts (default is 3).</param>
    /// <param name="baseDelayMs">The base delay in milliseconds (default is 500).</param>
    /// <example>
    /// <![CDATA[
    /// async Task SometimesFailsAsync()
    /// {
    ///     if (new Random().Next(2) == 0)
    ///         throw new Exception("Boom!");
    ///
    ///     Console.WriteLine("Async success!");
    /// }
    ///
    /// await RetryPolicy.ExecuteAsync(() => SometimesFailsAsync(), retryCount: 4, baseDelayMs: 300);
    /// ]]>
    /// </example>
    public static async Task ExecuteAsync(Func<Task> action, int retryCount = 3, int baseDelayMs = 500)
    {
        for (int attempt = 0; attempt < retryCount; attempt++)
        {
            try
            {
                await action();
                return;
            }
            catch
            {
                if (attempt == retryCount - 1) throw;

                int maxDelay = baseDelayMs * (int)Math.Pow(2, attempt);
                int jitter = _random.Next(0, maxDelay + 1);

                await Task.Delay(jitter);
            }
        }
    }

    /// <summary>
    /// Executes an asynchronous function with retry logic and returns a result, using exponential backoff and jitter.
    /// </summary>
    /// <typeparam name="T">Return type of the asynchronous function.</typeparam>
    /// <param name="func">The asynchronous function to execute.</param>
    /// <param name="retryCount">The number of retry attempts (default is 3).</param>
    /// <param name="baseDelayMs">The base delay in milliseconds (default is 500).</param>
    /// <returns>The result of the asynchronous function.</returns>
    /// <example>
    /// <![CDATA[
    /// async Task<int> GetNumberAsync()
    /// {
    ///     if (new Random().Next(3) != 0)
    ///         throw new Exception("No luck!");
    ///
    ///     return 123;
    /// }
    ///
    /// int number = await RetryPolicy.ExecuteAsync(() => GetNumberAsync(), retryCount: 5);
    /// Console.WriteLine(number);
    /// ]]>
    /// </example>
    public static async Task<T> ExecuteAsync<T>(Func<Task<T>> func, int retryCount = 3, int baseDelayMs = 500)
    {
        for (int attempt = 0; attempt < retryCount; attempt++)
        {
            try
            {
                return await func();
            }
            catch
            {
                if (attempt == retryCount - 1) throw;

                int maxDelay = baseDelayMs * (int)Math.Pow(2, attempt);
                int jitter = _random.Next(0, maxDelay + 1);
                await Task.Delay(jitter);
            }
        }

        throw new Exception("Retry failed.");
    }
}