Skip to content

Instantly share code, notes, and snippets.

@cajuncoding
Last active June 10, 2024 21:52
Show Gist options
  • Save cajuncoding/fe5f452af475110422fb15b0c55782c0 to your computer and use it in GitHub Desktop.
Save cajuncoding/fe5f452af475110422fb15b0c55782c0 to your computer and use it in GitHub Desktop.
Simple, but effective, asynchronous Retry mechanism with Exponential Backoff for C#
namespace CajunCoding
{
/// <summary>
/// Simple but effective Retry mechanism for C# with Exponential backoff and support for validating each result to determine if it should continue trying or accept the result.
/// https://en.wikipedia.org/wiki/Exponential_backoff
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="maxRetries">The Max number of attempts that will be made.</param>
/// <param name="action">The Func&lt;T&gt; process/action that will be attempted and will return generic type &lt;T&gt; when successful.</param>
/// <param name="validationAction">A dynamic validation rule that can determine if a given result of generic type &lt;T&gt; is acceptable or if the action should be re-attempted.</param>
/// <param name="initialRetryWaitTimeMillis">The initial delay time if the action fails; after which it will be exponentially expanded for longer delays with each iteration.</param>
/// <returns>Generic type &lt;T&gt; result from the Func&lt;T&gt; action specified.</returns>
/// <exception cref="AggregateException">Any and all exceptions that occur from all attempts made before the max number of retries was encountered.</exception>
public class Retry
{
public class RetryStatus
{
public RetryStatus(int failedAttemptCount = 0, int maxRetryAttemptCount = 0)
{
FailedAttemptCount = failedAttemptCount;
MaxRetryAttemptCount = maxRetryAttemptCount;
}
public int FailedAttemptCount { get; }
public int MaxRetryAttemptCount { get; }
public int RemainingRetryCount => MaxRetryAttemptCount - FailedAttemptCount;
public bool StopRetryProcess { get; private set; } = false;
public void StopRetrying() { StopRetryProcess = true; }
}
public static async Task<T> WithExponentialBackoffAsync<T>(
int maxRetries,
Func<RetryStatus, Task<T>> action,
int initialRetryWaitTimeMillis = 1000
)
{
var exceptions = new List<Exception>();
var maxRetryValidatedCount = Math.Max(maxRetries, 1);
//NOTE: We always make an initial attempt (index = 0, with NO delay) + the max number of retries attempts with
// exponential back-off delays; so for example with a maxRetries specified of 3 + 1 for the initial
// we will make a total of 4 attempts!
for (var failCount = 0; failCount <= maxRetryValidatedCount; failCount++)
{
var retryStatus = new RetryStatus(failCount);
try
{
//If we are retrying then we wait using an exponential back-off delay...
if (failCount > 0)
{
var powerFactor = Math.Pow(failCount, 2); //This is our Exponential Factor
var waitTimeSpan = TimeSpan.FromMilliseconds(powerFactor * initialRetryWaitTimeMillis); //Total Wait Time
await Task.Delay(waitTimeSpan).ConfigureAwait(false);
}
//Attempt the Action...
var result = await action(retryStatus).ConfigureAwait(false);
return result;
}
catch (Exception exc)
{
exceptions.Add(exc);
if (retryStatus.StopRetryProcess)
break;
}
}
//If we have Exceptions that were handled then we attempt to re-throw them so calling code can handle...
switch (exceptions.Count)
{
case 0: break;//DO NOTHING
case 1: throw exceptions.First();
default: throw new AggregateException(exceptions);
}
//Finally if no exceptions were handled (e.g. all failures were due to validateResult Func failing them) then we return the default (e.g. null)...
return default;
}
}
}
@cajuncoding
Copy link
Author

cajuncoding commented Jun 10, 2024

Advanced Usage:

int exponentialBackoffRetryAttempts = 10; //Read from configuration...
var timer = Stopwatch.StartNew();

return await Retry.WithExponentialBackoffAsync(exponentialBackoffRetryAttempts, async (retryStatus) =>
{
    var response = await service.DoSomeRequestsAsync(requestParams).ConfigureAwait(false);
    
    //Log the current retry status with details for how many attempts have been attempted, the max number, etc.
    if(retryStatus.FailedAttemptCount > 0)
        _log.LogTrace("Retrying Async Request [{ApiAttemptCount} of {MaxApiRetryCount}]...",
            retryStatus.FailedAttemptCount + 1,
            retryStatus.MaxRetryAttemptCount
        );

    //Manually stop any further retrying based on custom logic (here we halt if total time is greater than two minutes)...
    if(timer.Elapsed.TotalMinutes > 2)
        retryStatus.StopRetrying();

    return response;
}).ConfigureAwait(false)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment