Tuesday 9 February 2016

Retry Pattern for failed Invocations

There are numerous times when invoking a delegate may fail due to an unexpected exception occurring in which we would like to automatically retry the invocation up to a certain number of retries.  This can easily be achieved using the Retry Pattern as shown below:

public static bool TryInvoke<TException, T>(Func<T> del, out T value, out IList<Exception> exceptions, int attempts = 3, IInterval interval = null)
 where TException : Exception
{
 if (del == null) throw new ArgumentNullException("del");
 if (attempts <= 0) throw new ArgumentOutOfRangeException("attempts");

 exceptions = new List<Exception>();

 for (int i = 0; i < attempts; i++)
 {
  try
  {
   value = del();
   return true;
  }
  catch (TException ex)
  {
   exceptions.Add(ex);
   if (interval != null)
    interval.Sleep();
  }
 }

 value = default(T);
 return false;
}

The above generic implementation of the Retry Pattern also follows the structure of the Try-Parse Pattern in that if the invocation of the delegate fails on all of it's attempts then the "out" parameter for the return value of the delegate is set to it's default value and the method returns false.  Conversely, if the delegate succeeds then the return value of the delegate is assigned to the "out" parameter and returns true - this means we can invoke delegates as follows:

int value;
Func<int> calculateValue = null;
if (Invoker.TryInvoke<int>(calculateValue, out value))
{
 //print value
 Console.WriteLine("Value: {0}", value);
}
else
{
 //show error message to user
 Console.WriteLine("Error calculating value after X attempts");
}

It is also possible for us to gracefully retry on specific types of exceptions and throw exceptions on others.  For example, I may expect a failed attempt whilst downloading some data from an external source, therefore I may want to retry ONLY when a DownloadException is thrown, all other exceptions should cause an unhandled exception to be thrown:

string data;
Func<string> getData = null;
IList<Exception> exceptions;
try
{
 if (Invoker.TryInvoke<DownloadException, string>(getData, out data, out exceptions))
 {
  //succeeded
 }
 else
 {
  //gracefully handle exceptions
 }
}
catch (Exception ex)
{
 //unexpected exception
}

We may also want to run this logic asynchronously, for example when a user clicks a button on a GUI and requests to download some data from an external source:

Func<string> downloadData = () =>
{
 return new Downloader().Download();
};
var task = await Invoker.TryInvokeAsync<string>(downloadData);
if (task.Success)
{
 //invocation passed
 Console.WriteLine(task.Value);
}

Note how the syntax of the TryInvokeAsync method has changed slightly in that it no longer contains an out parameter for the assigned value - async methods are unable to support out parameters.

The complete source code of the Retry Invoker can be found on GitHub.