I need to enhance basic Retry pattern implementation for handling multiple types of exceptions. Say I want to implement a method (M) that re-attempts some action. And if that action causes an exception, main method catches it and passes to some exception evaluator (E). Now, the responsibility of the "E" is to return back an appropriate wait period to its caller (method M), who eventually enforces this delay. The "E" should also take into account the attempt for each type of occurred exception. For instance, "M" called 2 times on ConnectionLostException, and 3 times on "DatabaseInaccessibleException" I found similar, although not identical question here.
I did basic implementation that works without "E" method:
public enum IntervalGrowthRate { None, Linear, Exponential, Random };
public static async Task<T> RetryAsync<T>(
Func<Task<T>> action,
IDictionary<string, (IntervalGrowthRate, int, int)> retrySettings) {
int waitMs = 0;
int totalAttempts = 0;
Exception lastException = null;
IDictionary<string, int> retryAttempts = new Dictionary<string, int>();
while (true) {
try {
await Task.Delay(waitMs);
return await action().ConfigureAwait(false);
} catch (Exception ex) {
var exceptionName = ex.GetType().FullName;
if (retrySettings.TryGetValue(exceptionName, out var settings)) {
var intervalRate = settings.Item1;
var retryInterval = settings.Item2;
var retryCount = settings.Item3;
lastException = ex;
retryAttempts.TryGetValue(exceptionName, out int currentAttempt);
retryAttempts[exceptionName] = ++currentAttempt;
if (currentAttempt <= retryCount) {
waitMs = CalculateDelay(intervalRate, retryInterval, currentAttempt);
Logging.LogError("Hit an exception and will retry: {0}", activityId, ex.ToString());
totalAttempts++;
} else break;
}
else throw;
}
}
var exceptionMessage = string.Format($"{action.Method.Name} method execution failed after retrying {totalAttempts} times.");
throw new Exception(exceptionMessage, lastException);
}
private static int CalculateDelay(IntervalGrowthRate growthRate, int delayMs, int currentAttempt) {
// No delay necessary before the first attempt
if (currentAttempt < 1) {
return 0;
}
switch (growthRate){
case IntervalGrowthRate.Linear :
return delayMs * currentAttempt;
case IntervalGrowthRate.Exponential :
return delayMs * (int)Math.Pow(2, currentAttempt);
case IntervalGrowthRate.Random :
return (int)(delayMs * currentAttempt * (1 + new Random().NextDouble()));
case IntervalGrowthRate.None :
default :
return delayMs;
};
}
But the problem is that I need a more flexible logic for exception evaluation. Say look for keywords in an exception message, check InnerException, etc. Any help is appreciated!
EDIT: This is how to call the Retry:
var settings = new Dictionary<string, (IntervalGrowthRate, int, int)>()
{
["System.DivideByZeroException"] = (IntervalGrowthRate.Exponential, 1000, 2),
["System.OverflowException"] = (IntervalGrowthRate.Linear, 3000, 3)
};
var task = await RetryAsync(
async () =>
{
// do something that can trigger an exception
},
settings
);