4
\$\begingroup\$

I have created a class to try, catch and retry N times. My class also supports retrying asynchronously. I'm wondering if there are better ways to implement this. I'm most concerned with the HandleException and RetryLoop methods, but also about the design of the class as a whole. TryRetry GitHub

/// <summary>
/// Class for simulating Try, Catch, Retry (n) times.
/// </summary>
/// <typeparam name="TResult">Type to be returned.</typeparam>
public static class TryRetry<TResult>
{
    /// <summary>
    /// Handles exceptions thrown
    /// </summary>
    /// <param name="e">The <see cref="Exception"/> thrown.</param>
    /// <param name="exCatch"><see cref="IDictionary{TKey, TValue}"/> containing expected <see cref="Exception"/> <see cref="Type"/> 
    /// as key and <see cref="Func{TResult}"/> to invoke for that <see cref="Exception"/> as value.</param>
    /// <returns>Result of catch function.</returns>
    private static TResult HandleException(Exception e, IDictionary<Type, Func<TResult>> exCatch)
    {
        TResult result = default(TResult);
        bool handled = false;

        foreach (var ec in exCatch)
        {
            if (e.GetType() == ec.Key)
            {
                if (ec.Value != null) { result = ec.Value.Invoke(); }
                handled = true;
                break;
            }
        }

        if (!handled) throw e;
        return result;
    }

    /// <summary>
    /// Try, catch, then retry (n) times until max tries reached, an unexpected exception is thrown, 
    /// or try block executes without an exception.
    /// </summary>
    /// <param name="tryFunc">Try code block to execute.</param>
    /// <param name="exCatch"><see cref="IDictionary{TKey, TValue}"/> containing expected <see cref="Exception"/> <see cref="Type"/> 
    /// as key and <see cref="Func{TResult}"/> to invoke for that <see cref="Exception"/> as value.</param>
    /// <param name="maxTries">Maximum number of times to retry, minimum once.</param>
    /// <param name="millisecondsDelay">Milliseconds to delay next try.</param>
    /// <returns>tryFunc return value or catchFunc return value.</returns>
    private static TResult RetryLoop(Func<TResult> tryFunc, IDictionary<Type, Func<TResult>> exCatch,
        int maxTries = 1, int millisecondsDelay = 0)
    {
        TResult result = default(TResult);
        int numTries = 0;
        maxTries = maxTries < 0 ? 1 : maxTries;

        while (numTries <= maxTries)
        {
            try
            {
                result = tryFunc();
                break;
            }
            catch (Exception e)
            {
                result = HandleException(e, exCatch);
            }
            finally
            {
                numTries++;
                if (millisecondsDelay > 0 && numTries <= maxTries)
                {
                    System.Threading.Thread.Sleep(millisecondsDelay);
                }
            }
        }

        return result;
    }

    /// <summary>
    /// Try, catch, then retry (n) times until max tries reached, an unexpected exception is thrown, 
    /// or try block executes without an exception.
    /// </summary>
    /// <typeparam name="TException">Expected <see cref="Exception"/> to handle.</typeparam>
    /// <param name="tryFunc">Try code block to execute.</param>
    /// <param name="catchFunc">Catch code block to execute.</param>
    /// <param name="maxTries">Maximum number of times to retry, minimum once.</param>
    /// <param name="millisecondsDelay">Milliseconds to delay next try.</param>
    /// <returns>tryFunc return value or catchFunc return value.</returns>
    public static TResult Retry<TException>(Func<TResult> tryFunc, Func<TResult> catchFunc = null,
        int maxTries = 1, int millisecondsDelay = 0) where TException : Exception, new()
    {
        return RetryLoop(
            tryFunc,
            new Dictionary<Type, Func<TResult>>(){ { new TException().GetType(), catchFunc } },
            maxTries,
            millisecondsDelay);
    }

    /// <summary>
    /// Try, catch, then retry (n) times until max tries reached, an unexpected exception is thrown, 
    /// or try block executes without an exception.
    /// </summary>
    /// <param name="tryFunc">Try code block to execute.</param>
    /// <param name="exCatch"><see cref="IDictionary{TKey, TValue}"/> containing expected <see cref="Exception"/> <see cref="Type"/> 
    /// as key and <see cref="Func{TResult}"/> to invoke for that <see cref="Exception"/> as value.</param>
    /// <param name="maxTries">Maximum number of times to retry, minimum once.</param>
    /// <param name="millisecondsDelay">Milliseconds to delay next try.</param>
    /// <returns>tryFunc return value or catchFunc return value.</returns>
    public static TResult Retry(Func<TResult> tryFunc, IDictionary<Type, Func<TResult>> exCatch,
        int maxTries = 1, int millisecondsDelay = 0)
    {
        return RetryLoop(tryFunc, exCatch, maxTries, millisecondsDelay);
    }

    /// <summary>
    /// Try, catch, then retry (n) times asynchronously until max tries reached, an unexpected exception is thrown, 
    /// or try block executes without an exception.
    /// </summary>
    /// <param name="tryFunc">Try code block to execute.</param>
    /// <param name="catchFunc">Catch code block to execute.</param>
    /// <param name="maxTries">Maximum number of times to retry, minimum once.</param>
    /// <param name="millisecondsDelay">Milliseconds to delay next try.</param>
    /// <returns>Task</returns>
    public static async Task<TResult> RetryAsync<TException>(Func<TResult> tryFunc, Func<TResult> catchFunc = null,
        int maxTries = 1, int millisecondsDelay = 0) where TException : Exception, new()
    {
        return await Task.Run(() => RetryLoop(
            tryFunc,
            new Dictionary<Type, Func<TResult>>() { { new TException().GetType(), catchFunc } },
            maxTries,
            millisecondsDelay));
    }

    /// <summary>
    /// Try, catch, then retry (n) times asynchronously until max tries reached, an unexpected exception is thrown, 
    /// or try block executes without an exception.
    /// </summary>
    /// <param name="tryFunc">Try code block to execute.</param>
    /// <param name="exCatch"><see cref="IDictionary{TKey, TValue}"/> containing expected <see cref="Exception"/> <see cref="Type"/> 
    /// as key and <see cref="Func{TResult}"/> to invoke for that <see cref="Exception"/> as value.</param>
    /// <param name="maxTries">Maximum number of times to retry, minimum once.</param>
    /// <param name="millisecondsDelay">Milliseconds to delay next try.</param>
    /// <returns>Task</returns>
    public static async Task<TResult> RetryAsync(Func<TResult> tryFunc, IDictionary<Type, Func<TResult>> exCatch,
        int maxTries = 1, int millisecondsDelay = 0)
    {
        return await Task.Run(() => RetryLoop(tryFunc, exCatch, maxTries, millisecondsDelay));
    }
}
\$\endgroup\$
  • 2
    \$\begingroup\$ You might want to research the circuit breaker pattern for solving the "retry n times" problem in an object oriented way. \$\endgroup\$ – Mathieu Guindon May 23 '16 at 2:09
  • \$\begingroup\$ @Mat'sMug Very interesting, I like it. This looks like something I will most likely implement. Thanks! \$\endgroup\$ – Clint May 23 '16 at 3:01
  • 2
    \$\begingroup\$ My standard caution for this pattern: it does not handle composition well. If method A retries ten times before throwing, and method B calls method A and it retries ten times before throwing, and method C calls method B and it retries ten times before throwing, then we actually try operation A a thousand times. If A introduces a delay then the user could be waiting minutes or hours. Retry logic is one of those things that requires global understanding; either B needs to understand that A has already been retried, or it needs to not retry A. \$\endgroup\$ – Eric Lippert May 23 '16 at 14:13
  • \$\begingroup\$ @EricLippert I have been thinking about this and I might leave it up to the developer. The only solution I could come up with was to have a parameter of a unique id for that Retry call. \$\endgroup\$ – Clint May 23 '16 at 21:08
  • 1
    \$\begingroup\$ @Clint: As a colleague of mine used to say "every time we let a developer write a line of code, that's one more opportunity for a bug". Personally I am not a big fan of retry logic; most of the time, if something failed once it will fail again. There are of course exceptions. The last time I wrote retry logic it was for attempting to close a file and then re-open it again; badly-written virus checkers will not wait for a possible re-open before locking the file to examine it to see if I just wrote a virus into it, so the close-reopen pattern needs a retry. Vexing! \$\endgroup\$ – Eric Lippert May 23 '16 at 21:12
4
\$\begingroup\$

I see 2 little issues with RetryLoop method.

First, you check maxTries for an invalid negative value but I think that a 0 value is equally invalid. So I would suggest this change:

maxTries = maxTries <= 0 ? 1 : maxTries;

Or equally you could use:

maxTries = Max(maxTries, 1);

The other things is when you break out of the while loop thanks to not having an exception, the finally block still executes, which means you have to wait up to millisecondsDelay once more than needed.

I would suggest this change:

try
{
    result = tryFunc();
    numTries = maxTries;
    break;
}

Then there is no delay in the finally block!

\$\endgroup\$
  • \$\begingroup\$ Nice catches! I like your Math.Max() solution, it's much easier to read. For the finally block, I think I would rather pull the code out and put it at the end of the loop. Thank you for your input! \$\endgroup\$ – Clint May 22 '16 at 19:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.