Last active
June 10, 2024 21:52
-
-
Save cajuncoding/fe5f452af475110422fb15b0c55782c0 to your computer and use it in GitHub Desktop.
Simple, but effective, asynchronous Retry mechanism with Exponential Backoff for C#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<T> process/action that will be attempted and will return generic type <T> when successful.</param> | |
/// <param name="validationAction">A dynamic validation rule that can determine if a given result of generic type <T> 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 <T> result from the Func<T> 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; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Advanced Usage: