Skip to content

Instantly share code, notes, and snippets.

@jpdillingham
Created December 15, 2023 20:08
Show Gist options
  • Save jpdillingham/bd433b8841b6e28cf037c53199254f05 to your computer and use it in GitHub Desktop.
Save jpdillingham/bd433b8841b6e28cf037c53199254f05 to your computer and use it in GitHub Desktop.
namespace Navix
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Retry logic. Stolen from one of my side projects.
/// </summary>
public static class Retry
{
/// <summary>
/// Executes logic with the specified retry parameters.
/// </summary>
/// <param name="task">The logic to execute.</param>
/// <param name="isRetryable">A function returning a value indicating whether the last Exception is retryable.</param>
/// <param name="onFailure">An action to execute on failure.</param>
/// <param name="maxAttempts">The maximum number of retry attempts.</param>
/// <param name="maxDelayInMilliseconds">The maximum delay in milliseconds.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The execution context.</returns>
public static async Task Do(Func<Task> task, Func<int, Exception, bool> isRetryable = null, Action<int, Exception> onFailure = null, int maxAttempts = 3, int maxDelayInMilliseconds = int.MaxValue, CancellationToken cancellationToken = default)
{
await Do<object>(async () =>
{
await task();
return Task.FromResult<object>(null);
}, isRetryable, onFailure, maxAttempts, maxDelayInMilliseconds, cancellationToken);
}
/// <summary>
/// Executes logic with the specified retry parameters.
/// </summary>
/// <param name="task">The logic to execute.</param>
/// <param name="isRetryable">A function returning a value indicating whether the last Exception is retryable.</param>
/// <param name="onFailure">An action to execute on failure.</param>
/// <param name="maxAttempts">The maximum number of retry attempts.</param>
/// <param name="maxDelayInMilliseconds">The maximum delay in miliseconds.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <typeparam name="T">The Type of the logic return value.</typeparam>
/// <returns>The execution context.</returns>
public static async Task<T> Do<T>(Func<Task<T>> task, Func<int, Exception, bool> isRetryable = null, Action<int, Exception> onFailure = null, int maxAttempts = 3, int maxDelayInMilliseconds = int.MaxValue, CancellationToken cancellationToken = default)
{
isRetryable ??= (_, _) => true;
onFailure ??= (_, _) => { };
var exceptions = new List<Exception>();
for (int attempts = 0; attempts < maxAttempts; attempts++)
{
if (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException();
}
try
{
if (attempts > 0)
{
var (delay, jitter) = ExponentialBackoffDelay(attempts, maxDelayInMilliseconds);
await Task.Delay(delay + jitter, cancellationToken);
}
return await task();
}
catch (Exception ex)
{
exceptions.Add(ex);
try
{
onFailure(attempts + 1, ex);
if (!isRetryable(attempts + 1, ex))
{
break;
}
}
catch (Exception retryEx)
{
throw new RetryException($"Failed to retry operation: {retryEx.Message}", ex);
}
}
}
throw new AggregateException(exceptions);
}
public static (int Delay, int Jitter) ExponentialBackoffDelay(int iteration, int maxDelayInMilliseconds = int.MaxValue)
{
iteration = Math.Min(100, iteration);
var computedDelay = Math.Floor((Math.Pow(2, iteration) - 1) / 2) * 1000;
var clampedDelay = (int)Math.Min(computedDelay, maxDelayInMilliseconds);
var jitter = new Random().Next(1000);
return (clampedDelay, jitter);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment