Coder Perfect

What is the most efficient approach to write retry logic?

Problem

I sometimes have to retry an operation multiple times before giving up. My code looks like this:

int retries = 3;
while(true) {
  try {
    DoSomething();
    break; // success!
  } catch {
    if(--retries == 0) throw;
    else Thread.Sleep(1000);
  }
}

I’d like to rewrite this in the form of a general retry function, such as:

TryThreeTimes(DoSomething);

Is it possible to do this in C#? What would the TryThreeTimes() method’s code be?

Asked by noctonura

Solution #1

If utilized as a general exception management mechanism, blanket catch statements that merely retry the same function can be problematic. With that in mind, here’s a lambda-based retry wrapper you can use with any method. I chose to factor the number of retries and the retry timeout out as parameters for a bit more flexibility:

public static class Retry
{
    public static void Do(
        Action action,
        TimeSpan retryInterval,
        int maxAttemptCount = 3)
    {
        Do<object>(() =>
        {
            action();
            return null;
        }, retryInterval, maxAttemptCount);
    }

    public static T Do<T>(
        Func<T> action,
        TimeSpan retryInterval,
        int maxAttemptCount = 3)
    {
        var exceptions = new List<Exception>();

        for (int attempted = 0; attempted < maxAttemptCount; attempted++)
        {
            try
            {
                if (attempted > 0)
                {
                    Thread.Sleep(retryInterval);
                }
                return action();
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
        throw new AggregateException(exceptions);
    }
}

This utility method can now be used to perform retry logic:

Retry.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));

or:

Retry.Do(SomeFunctionThatCanFail, TimeSpan.FromSeconds(1));

or:

int result = Retry.Do(SomeFunctionWhichReturnsInt, TimeSpan.FromSeconds(1), 4);

Alternatively, you might create an async overload.

Answered by LBushkin

Solution #2

You should give Polly a shot. It’s a.NET package that lets developers describe transient exception handling policies like Retry, Retry Forever, Wait and Retry, and Circuit Breaker in a fluent way.

Policy
    .Handle<SqlException>(ex => ex.Number == 1205)
    .Or<ArgumentException>(ex => ex.ParamName == "example")
    .WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(3))
    .Execute(() => DoSomething());

Answered by Michael Wolfenden

Solution #3

public void TryThreeTimes(Action action)
{
    var tries = 3;
    while (true) {
        try {
            action();
            break; // success!
        } catch {
            if (--tries == 0)
                throw;
            Thread.Sleep(1000);
        }
    }
}

Then you’d make a call to:

TryThreeTimes(DoSomething);

…or alternatively…

TryThreeTimes(() => DoSomethingElse(withLocalVariable));

A more adaptable option is:

public void DoWithRetry(Action action, TimeSpan sleepPeriod, int tryCount = 3)
{
    if (tryCount <= 0)
        throw new ArgumentOutOfRangeException(nameof(tryCount));

    while (true) {
        try {
            action();
            break; // success!
        } catch {
            if (--tryCount == 0)
                throw;
            Thread.Sleep(sleepPeriod);
        }
   }
}

To be employed as:

DoWithRetry(DoSomething, TimeSpan.FromSeconds(2), tryCount: 10);

A more current version with async/await support:

public async Task DoWithRetryAsync(Func<Task> action, TimeSpan sleepPeriod, int tryCount = 3)
{
    if (tryCount <= 0)
        throw new ArgumentOutOfRangeException(nameof(tryCount));

    while (true) {
        try {
            await action();
            return; // success!
        } catch {
            if (--tryCount == 0)
                throw;
            await Task.Delay(sleepPeriod);
        }
   }
}

To be employed as:

await DoWithRetryAsync(DoSomethingAsync, TimeSpan.FromSeconds(2), tryCount: 10);

Answered by Drew Noakes

Solution #4

This could be a poor idea. For starters, it exemplifies the adage that “doing the same thing twice and expecting different results each time” is the definition of insanity. Second, this code pattern does not work well with other coding patterns. Consider the following scenario:

Let’s imagine your network hardware layer resends a packet three times when it fails, each time waiting a second.

Assume the software layer resends a failure warning three times in response to a packet failure.

Assume that a notice delivery failure causes the notification layer to reactivate the notification three times.

Assume the notification layer is reactivated three times after a notification failure by the error reporting layer.

Let’s say the web server reactivates error reporting three times after an issue occurs.

Let’s say the web client receives an error from the server and resends the request three times.

Assume the network switch line that is meant to send the message to the administrator is disconnected. When does the web client’s user receive their error message? I make it at about twelve minutes later.

We’ve seen this error in customer code, however it’s far, far worse than I’ve detailed here, lest you think this is just a ridiculous example. Because so many layers were automatically retrying with waits, the time between the error condition occurring and it finally being notified to the user was several weeks in this particular customer code. Consider what would happen if there were ten retries available rather than three.

When an error circumstance occurs, the best course of action is to report it immediately and let the user decide what to do. Allow the user to set an automatic retry policy at the appropriate level in the software abstraction if they so desire.

Answered by Eric Lippert

Solution #5

The Transient Fault Handling Application Block has a set of retry strategies that can be expanded.

It also provides a set of cloud-based service error detection strategies.

This chapter of the Developer’s Guide contains extra information.

Topaz is a NuGet package (search for ‘topaz’).

Answered by Grigori Melnik

Post is based on https://stackoverflow.com/questions/1563191/cleanest-way-to-write-retry-logic