Skip to content

Instantly share code, notes, and snippets.

@icalvo
Last active August 29, 2015 14:08
Show Gist options
  • Save icalvo/3f5602f7e868e7a00690 to your computer and use it in GitHub Desktop.
Save icalvo/3f5602f7e868e7a00690 to your computer and use it in GitHub Desktop.
Retry logic, generic enough to manage lots of possible cases. As an example of its genericity, a reimplementation of one method of gist https://gist.github.com/gazlu/7886208. See my comment for details.
public class Retrier<TResult> {
public static TResult Try(Func<TResult> func, int maxRetries, TResult expectedValue, int delayInMilliseconds = 0)
{
FunctionExecutor executor = new FunctionExecutor(func);
try
{
GenericRetrier.Retry(
executor.Execute,
maxRetries,
() => executor.Result.Equals(expectedValue),
(ex, time) => Thread.Sleep(delayInMilliseconds));
}
catch (AggregateException)
{
}
return default(TResult);
}
private class FunctionExecutor
{
private readonly Func<TResult> _func;
public FunctionExecutor(Func<TResult> func)
{
_func = func;
}
public TResult Result { get; private set; }
public void Execute()
{
Result = _func();
}
}
}
public class ResultCheckException : Exception
{
public ResultCheckException(string message)
: base(message)
{
}
}
/// <summary>
/// Utilities for retrying a block of code a number of times.
/// </summary>
public static class GenericRetrier
{
/// <summary>
/// Retries the specified action the specified number of times.
/// The condition for retry is either throwing an exception in the action,
/// or failing the result check (if provided).
/// When retrying, it executes a handler (if provided).
/// </summary>
/// <param name="action">The action.</param>
/// <param name="resultChecker">The result checker.</param>
/// <param name="numberOfTimes">The number of times.</param>
/// <param name="retryExceptionHandler">The retry exception handler.</param>
/// <returns>
/// Result provided by the action.
/// </returns>
/// <exception cref="AggregateException">If the action to be invoked fails for more than <see cref="numberOfTimes" />.</exception>
public static void Retry(Action action, int numberOfTimes, Func<bool> resultChecker = null,
Action<Exception, int> retryExceptionHandler = null)
{
List<Exception> exceptions = new List<Exception>();
for (int i = 0; i < numberOfTimes; i++)
{
try
{
action();
if (resultChecker == null || resultChecker())
{
return;
}
throw new ResultCheckException("The action result check has failed");
}
catch (Exception ex)
{
if (i < numberOfTimes - 1)
{
if (retryExceptionHandler != null)
{
try
{
retryExceptionHandler(ex, i + 1);
}
catch (Exception)
{
}
}
}
exceptions.Add(ex);
}
}
throw new AggregateException(exceptions);
}
}
@icalvo
Copy link
Author

icalvo commented Oct 31, 2014

Rationales:

  • Static class & function: we don't need to instantiate anything.
  • No result returning: it is certainly an command; if you need results you should execute methods on objects and query those objects later.
  • Result checking: provided because there are one thousand ways to check if you must retry, and also to promote CQS (first do the command, then query the results and decide if you want a retry).
  • retryExceptionHandler only works when you are going to retry: if you are out of attempts, Retry has failed, so it will launch an AggregateException and you can manage that case outside.
  • Try-Catch-all when calling retryExceptionHandler: I didn't want to add this exception to the final AggregateException so that it contains numberOfTimes exceptions. If you need this exception and don't matter about matching the attempt # and the exceptions it caused, you could add the exception to the list. If you need both things, you will need a more complex structure; for example you could create an AggregateException with the exceptions caused by action and retryExceptionHandler and add it to the list.
  • Final AggregateException: nice way to provide to the client all the exceptions for each of the failures.
  • ResultCheckerException: this way all the failures (exception and result checker failures) are treated the same way (with an exception).

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