Cleanest way to write retry logic?
Asked Answered
J

31

543

Occasionally I have a need to retry an operation several times before giving up. My code is like:

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

I would like to rewrite this in a general retry function like:

TryThreeTimes(DoSomething);

Is it possible in C#? What would be the code for the TryThreeTimes() method?

Jene answered 13/10, 2009 at 22:3 Comment(4)
A simple cycle is not enough? Why just not to iterate over and execute logic for several times?Parimutuel
Personally, I would be extremely wary of any such helper method. It's certainly possible to implement using lambdas, but the pattern itself is extremely smelly, so introducing a helper for it (which implies that it is frequently repeated) is in and of itself highly suspicious, and strongly hints at bad overall design.Pyxie
In my case, my DoSomething()s are doing stuff on remote machines such as deleting files, or trying to hit a network port. In both cases, there are major timing issues for when DoSomething will succeed and because of the remoteness, there is no event I can listen on. So yeah, its smelly. Suggestions welcome.Jene
@PavelMinaev why would using retries hint at bad overall design? If you write a lot of code that connects integration points then using retries is definitely a pattern you should seriously consider using.Granivorous
G
645

Blanket catch statements that simply retry the same call can be dangerous if used as a general exception handling mechanism. Having said that, here's a lambda-based retry wrapper that 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);
    }
}

You can now use this utility method 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);

Or you could even make an async overload.

Gaivn answered 13/10, 2009 at 22:10 Comment(23)
+1, especially for the warning and error-checking. I'd be more comfortable if this passed in the type of the exception to catch as a generic parameter (where T: Exception), though.Surgeon
@TrueWill, agreed, I solved it with the ugly Type argument in RetryForExcpetionType below; however the principal of looking for a specific exception rather than any exception should be applied.Novercal
@LBushkin, the method action() executes 4 times when you specify numRetries == 3. Was that the intent? If so you should call attention to the difference in behavior to the OP implementation. I think your way makes more sense when you stop and think about it.Novercal
It was my intent that "retries" actually meant retries. But it's not too hard to change it to mean "tries". As long as the name is kept meaningful. There are other opportunities to improve the code, like checking for negative retries, or negative timeouts - for example. I omitted these mostly to keep the example simple ... but again, in practice these would probably be good enhancements to the implementation.Gaivn
Move the if out of the do loop, for God's sake!Jovi
@Eduardo: Moving the if out of the do loop would result in the catch statement sleeping even when the retry count is zero. Not something that I think is generally desirable. Do you have a way to avoid that without the conditional check inside the loop?Gaivn
Ya I noticed the retry would happen > once on success but the answer to use Action (which I was not familiar with) is the main thing I was looking for.Jene
As mentioned in RichAmberale's comment, there's a bug! This code will execute action multiple times even when there are no exceptions. Surely action should only be called again if the previous call(s) failed.Apuleius
Action is a .NET framework type that is accessible from VB. VB2008 also has expression lambdas, but not statement lambdas (though the latter will be in VB2010).Pyxie
Additionally you don't want to ever retry if the exception has a "fatal" nature, like ExecutionEngineException, OutOfMemoryException, AccessViolationException, ThreadAbortException, etc.Klarrisa
We use a similar pattern for our DB access in a high volume Biztalk App, but with two improvements: We have blacklists for exceptions that shouldn't be retried and we store the first exception that occurs and throw that when the retry ultimately fails. Reason being that the second and following exceptions often are different from the first one. In that case you hide the initial problem when rethrowing only the last exception.Kelleekelleher
@Kelleekelleher When you say throw your first exception, will it clear the stack trace?Scandinavian
@Scandinavian We throw a new exception with the original exception as inner exception. The original stack trace is available as attribute from the inner exceptions.Kelleekelleher
I think you can use while(true) instead of while( numRetries-- > 0 ) and decrease the retries in the if-statement if( numRetries-- <= 0 ) throw;Fredette
3 bugs are here. At least for .net 4.0.Overmatter
Suggestion: Pass in an exception as a Type (defaulted to null), and only perform the retry if a specific exception matches the passed in type.Splat
Also note that in case of client timeout, it could be possible that on the server side the transaction WAS completed, but just not in the timeout interval the client side had defined. This would mean the same action would be executed multiple times. E.g.: the action provided as parameter calls an external service. That service takes 2 minutes to complete. However, the action has specified a timeout of 1 minute. This will result in the action being executed multiple times on the server side, even though your code will throw the AggregateException.Banking
I would change the inner retry loop to: try { if (retry > 0) Thread.Sleep(retryInterval); ... So you don't actually delay after the last attempt was executed.Labio
You could also try using an open source library such as Polly to handle this. There is much more flexibility for waiting between retries and it has been validated by many others that have used the project. Example: Policy.Handle<DivideByZeroException>().WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });Cartoon
I very much like this approach for simplicity and amount of control. I picked it up in this very response and would like to share an addition I developed. Instead of maxAttemptCount, I created an IRetryHandler to be used in this method with fallback to a simple default implementation. It should take the exceptions as its argument and must count the tries and whatever infrastructure required for proper handling. This allows you fine control of the handling and retry strategy while keeping it separate from the actual retry logic which is simple.Tortola
does Thread.Sleep block that thread or does it yield and wakes thread up to check timer? The latter is fine. However, the first can consume many threads all at onceRhumb
Why is this so convoluted? You create a generic method just to make it return null, then you have to type T as object to make it compile. Did you copy it from somewhere?Scotsman
@SaherAhwal It will block the thread. It probably is better to use await Task.Delay(retryInterval); as said in #8816395Smuts
P
279

You should try Polly. It's a .NET library written by me that allows developers to express transient exception handling policies such as Retry, Retry Forever, Wait and Retry or Circuit Breaker in a fluent manner.

Example

Policy
    .Handle<SqlException>(ex => ex.Number == 1205)
    .Or<ArgumentException>(ex => ex.ParamName == "example")
    .WaitAndRetry(3, _ => TimeSpan.FromSeconds(3))
    .Execute(DoSomething);
Probation answered 5/5, 2013 at 8:41 Comment(4)
What is actually the OnRetry delegate? I assume it is what we need to perform when exception is occurred. So when exception is occurred OnRetry delegate will call and afterwards Execute delegate. Is it so?Antilog
Where I should use this snippet code? if the answer is startup.cs, how to register Policy?Nanine
Q: What is actually the OnRetry delegate? A: It only allows you to do something when a retry is performed (e.g., log something). You don't need to call Execute in there, that happens automatically.Swanee
@SinaRiani You can do something like this with Polly. https://mcmap.net/q/73534/-cleanest-way-to-write-retry-logicGuyton
R
77
public void TryThreeTimes(Action action)
{
    var tries = 3;
    while (true) {
        try {
            action();
            break; // success!
        } catch {
            if (--tries == 0)
                throw;
            Thread.Sleep(1000);
        }
    }
}

Then you would call:

TryThreeTimes(DoSomething);

...or alternatively...

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

A more flexible option:

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 used as:

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

A more modern version with support for async/await:

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 used as:

await DoWithRetryAsync(DoSomethingAsync, TimeSpan.FromSeconds(2), tryCount: 10);
Ragtime answered 13/10, 2009 at 22:8 Comment(6)
preferably change the if to: --retryCount <= 0 because this will go on forever if you want to disable retries by setting it to 0. Technically the term retryCount isn't a really good name, because it won't retry if you set it to 1. either rename it to tryCount or put the -- behind.Gasify
@saille I agree. However the OP (and all other answers) are using Thread.Sleep. Alternatives are to use timers, or more likely nowadays to use async to retry, with Task.Delay.Ragtime
I've added an async version.Ragtime
Only break if action returns true ? Func<bool>Inalterable
@DrewNoakes Is there any benefits to use async version than the the other versions?Cram
@Cram you'd only use the async version if you want to free the thread up to do other work, rather than sleeping. If you run work on the thread pool, for example, you shouldn't be putting those threads to sleep.Ragtime
U
57

This is possibly a bad idea. First, it is emblematic of the maxim "the definition of insanity is doing the same thing twice and expecting different results each time". Second, this coding pattern does not compose well with itself. For example:

Suppose your network hardware layer resends a packet three times on failure, waiting, say, a second between failures.

Now suppose the software layer resends a notification about a failure three times on packet failure.

Now suppose the notification layer reactivates the notification three times on a notification delivery failure.

Now suppose the error reporting layer reactivates the notification layer three times on a notification failure.

And now suppose the web server reactivates the error reporting three times on error failure.

And now suppose the web client resends the request three times upon getting an error from the server.

Now suppose the line on the network switch that is supposed to route the notification to the administrator is unplugged. When does the user of the web client finally get their error message? I make it at about twelve minutes later.

Lest you think this is just a silly example: we have seen this bug in customer code, though far, far worse than I've described here. In the particular customer code, the gap between the error condition happening and it finally being reported to the user was several weeks because so many layers were automatically retrying with waits. Just imagine what would happen if there were ten retries instead of three.

Usually the right thing to do with an error condition is report it immediately and let the user decide what to do. If the user wants to create a policy of automatic retries, let them create that policy at the appropriate level in the software abstraction.

Underneath answered 14/10, 2009 at 5:52 Comment(22)
+1. Raymond shares a real life example here, blogs.msdn.com/oldnewthing/archive/2005/11/07/489807.aspxChemise
-1 This advice is useless for transient network failures encountered by automated batch processing systems.Leclair
Not sure if this is saying "Don't do it" followed by "do it". Most of the people asking this question are probably the people working in the software abstraction.Jory
When you have long running batch jobs that use network resources, such as web services, you can't expect the network to be 100% reliable. There are going to be occasional timeouts, socket disconnects, possibly even spurious routing glitches or server outages that occur while you are using it. One option is to fail, but that may mean restarting a lengthy job later. Another option is to retry a few times with suitable delay to see if it's a temporary problem, then fail. I agree about composition, which you have to be aware of.. but it's sometimes the best choice.Mammy
As @Mystere Man says, there is situations where it doesn't necessarily serve the user to get hit by a retry dialog instantly. But if you must, the scenario here is grim, so don't ever use "Retry X number of times"... if you must retry automatically, then use "Retry every X for Y time", e.g. "Retry every 100 ms for 500 ms"... Then you can ensure that it won't grow in time for each layer, because the top layers would never retry as the maximum time would be exceeded by the lower layer retries.Usm
I think that the quote you used at the beginning of your answer is interesting. "Expecting different results" is only insanity if prior experience regularly gives you the same results. While software is built on a promise of consistency, there are definitely circumstances where we are required to interact with unreliable forces outside of our control.Madea
@probackpacker: Sure -- the question then is "over what time scale is the unpredictable force likely to change?" A power spike that makes a network router unresponsive for a millisecond, and someone powering off the router for an hour, are very different things! (Another way to criticize my quote is that of course that is not anything even vaguely like the actual definition of insanity.)Underneath
@probackpacker: Case in point: a situation where I do use the "try, try again" approach is when I am writing a file, closing it, and then opening it again soon after. (Think of a log file used to diagnose a program that is crashing unpredictably; I don't know when the last close is going to be and I don't want to lose any data.) It is very common for badly-written virus checkers to lock the file the moment it is closed and then spend a few milliseconds checking it for viruses; if I attempt to open the file in those few milliseconds it will fail, but likely succeed on the second try.Underneath
@EricLippert: I couldn't agree more, and I still think your answer is valuable (+1 btw). Thanks for your clarification of a situation where you do use retry logic. I'm currently working with some C# code that uses async/await to repeatedly write to the database. Sometimes everything goes well, sometimes it fails miserably, and it's unclear as to why. In light of that, I read your answer and couldn't help but reflect on the assumption behind the quote.Madea
Actually I think it is the definition of non-deterministic.Monophonic
When you have an automated process that is connecting to databases or reading data from the web in general, things may go wrong, but can be solved just by adding a retry logic. I would say it is mandatory to implement retry logic for these cases.Thundershower
@JuanAndrésCidMolina: When you have an automated process that is connecting to network resources, things may go wrong, and can be made orders of magnitude worse just by adding retry logic. I would say it is mandatory to NOT implement retry logic in those cases. I am more interested in avoiding harm to the user than avoiding inconvenience.Underneath
@EricLippert true that. It actually depends on what you are doing.Thundershower
Poor advice. For example, the exponential backoff pattern is well-known and commonly used in large distributed systems. Here is one of many sources referencing this pattern: learn.microsoft.com/en-us/dotnet/architecture/microservices/… and the correct answer should guide the OP towards this pattern, instead of simply saying "don't do it".Depose
@BartoszKP: I am somewhat confused by your comment. How does exponentially backing off retries prevent long delays caused by composition of this pattern with itself? It seems to me that it would make that worse, not better. Exponential backoff mitigates unwanted DOSsing of one's own service; to my understanding it is not a mitigation of the issue I raised in this answer, which is about composition. Can you explain?Underneath
@EricLippert Yes, we are talking about different things. However, your answer as it is, to the question as it is reads to me "don't do it". This is poor advice since there are valid reasons to do it, and commonly used patterns to do it (hence my example). Thus suggesting it is a bad idea overall is misleading IMHO, especially if what follows is a lot of "suppose" for a very specific example - assuming OP is asking about this type of errors (where most probably they are not). And also, the maxim you quote is irrelevant in systems where transient errors do happen.Depose
@BartoszKP: I take your point. However. pointing out "that maxim does not apply in all the cases where it does not apply" is not an actionable criticism. "X doesn't apply in the cases where it does not apply" is a tautology.Underneath
@EricLippert You start with this maxim as a standalone argument (you say "First", the whole specific example is attached to the part you start with "Second"). I'm sure you didn't mean it, but this first part feels like talking down to someone who was silly enough to even think about doing something twice and expecting different results :-) Sorry if this feels like nitpicking, I honestly think that while the second part of your answer is a valuable example, the first part starts off with the reader on the wrong foot.Depose
@BartoszKP: You've started with an irrelevant technical critique which you admit does not have anything to do with the issue I've raised in the answer, and now you've moved on to a critique of tone. I'll only be responding further to substantive critiques on technical merits, rather than on word choice and your interpretation of my "tone". I encourage you to limit your critiques to substantive ones in general.Underneath
@EricLippert The technical critique is relevant - you present a specific scenario and claim (see your first point) that it disproves the idea in general, which is technically incorrect. I encourage you not to skip the technical part of my comments instead of focusing on the non-technical suggestion. I think I have nothing more to add, so I'll stop here - have a good day/night and thank you for your time.Depose
devblogs.microsoft.com/oldnewthing/archive/2005/11/07/… not foundInalterable
@Kiquenet: devblogs.microsoft.com/oldnewthing/20051107-20/?p=33433Underneath
U
34

The Transient Fault Handling Application Block provides an extensible collection of retry strategies including:

  • Incremental
  • Fixed interval
  • Exponential back-off

It also includes a collection of error detection strategies for cloud-based services.

For more information see this chapter of the Developer's Guide.

Available via NuGet (search for 'topaz').

Unaunabated answered 15/8, 2012 at 17:1 Comment(6)
Interesting. Can you use this outside of Windows Azure, say in a Winforms app?Gian
Absolutely. Use the core retry mechanism and provide your own detection strategies. We intentionally decoupled those. Find the core nuget package here: nuget.org/packages/TransientFaultHandling.CoreUnaunabated
Updated doc can be found here: msdn.microsoft.com/en-us/library/dn440719(v=pandp.60).aspxUnaunabated
Also, the project is now under Apache 2.0 and accepting community contributions. aka.ms/entlibopenUnaunabated
@Alex. The pieces of it are making it into the platform.Unaunabated
This is now deprecated, and last I used it it contained some bugs that as far as I know weren't, and never will be fixed: github.com/MicrosoftArchive/….Minyan
W
18

I'm a fan of recursion and extension methods, so here are my two cents:

public static void InvokeWithRetries(this Action @this, ushort numberOfRetries)
{
    try
    {
        @this();
    }
    catch
    {
        if (numberOfRetries == 0)
            throw;

        InvokeWithRetries(@this, --numberOfRetries);
    }
}
Waechter answered 13/2, 2013 at 8:13 Comment(0)
R
15

Allowing for functions and retry messages

public static T RetryMethod<T>(Func<T> method, int numRetries, int retryTimeout, Action onFailureAction)
{
 Guard.IsNotNull(method, "method");            
 T retval = default(T);
 do
 {
   try
   {
     retval = method();
     return retval;
   }
   catch
   {
     onFailureAction();
      if (numRetries <= 0) throw; // improved to avoid silent failure
      Thread.Sleep(retryTimeout);
   }
} while (numRetries-- > 0);
  return retval;
}
Regolith answered 7/1, 2010 at 23:13 Comment(2)
RetryMethod to retval are True , or max retries ?Inalterable
If more retries, more retryTimeout time. Or combine with github.com/David-Desmaisons/RateLimiterInalterable
N
14

You might also consider adding the exception type you want to retry for. For instance is this a timeout exception you want to retry? A database exception?

RetryForExcpetionType(DoSomething, typeof(TimeoutException), 5, 1000);

public static void RetryForExcpetionType(Action action, Type retryOnExceptionType, int numRetries, int retryTimeout)
{
    if (action == null)
        throw new ArgumentNullException("action");
    if (retryOnExceptionType == null)
        throw new ArgumentNullException("retryOnExceptionType");
    while (true)
    {
        try
        {
            action();
            return;
        }
        catch(Exception e)
        {
            if (--numRetries <= 0 || !retryOnExceptionType.IsAssignableFrom(e.GetType()))
                throw;

            if (retryTimeout > 0)
                System.Threading.Thread.Sleep(retryTimeout);
        }
    }
}

You might also note that all of the other examples have a similar issue with testing for retries == 0 and either retry infinity or fail to raise exceptions when given a negative value. Also Sleep(-1000) will fail in the catch blocks above. Depends on how 'silly' you expect people to be but defensive programming never hurts.

Novercal answered 13/10, 2009 at 22:46 Comment(6)
+1, but why not do RetryForException<T>(...) where T: Exception, then catch(T e)? Just tried it and it works perfectly.Surgeon
Either or here since I don't need to do anything with the Type provided I figured a plain old parameter would do the trick.Novercal
@Surgeon apparently catch(T ex) has some bugs according to this post #1578260Novercal
Update: Actually a better implementation I've been using takes a Predicate<Exception> delegate that returns true if a retry is appropriate. This allows you to use native error codes or other properties of the exception to determine if a retry is applicable. For instance HTTP 503 codes.Novercal
@csharptest.net: the SO link you posted actually concludes that the bug is only evident under the VS debugger (with .NET 3.5). I've tested catch (T ex) and it works perfectly in VS 2010 both under the debugger and otherwiseLempres
"Also Sleep(-1000) will fail in the catch blocks above" ... use a TimeSpan and you won't get this problem. Plus TimeSpan is much more flexible and self descriptive. From your signature of "int retryTimeout" how do i know if retryTimeout is MS, seconds, minutes, years?? ;-)Granivorous
G
11

Implemented LBushkin's answer in the latest fashion:

    public static async Task Do(Func<Task> task, TimeSpan retryInterval, int maxAttemptCount = 3)
    {
        var exceptions = new List<Exception>();
        for (int attempted = 0; attempted < maxAttemptCount; attempted++)
        {
            try
            {
                if (attempted > 0)
                {
                    await Task.Delay(retryInterval);
                }

                await task();
                return;
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
        throw new AggregateException(exceptions);
    }

    public static async Task<T> Do<T>(Func<Task<T>> task, TimeSpan retryInterval, int maxAttemptCount = 3)
    {
        var exceptions = new List<Exception>();
        for (int attempted = 0; attempted < maxAttemptCount; attempted++)
        {
            try
            {
                if (attempted > 0)
                {
                    await Task.Delay(retryInterval);
                }
                return await task();
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
        throw new AggregateException(exceptions);
    }  

and to use it:

await Retry.Do([TaskFunction], retryInterval, retryAttempts);

whereas the function [TaskFunction] can either be Task<T> or just Task.

Good answered 2/3, 2018 at 14:42 Comment(2)
Thank you, Fabian! This should be upvoted all the way to the top!Schroeder
@MarkLauter the short answer is yes. ;-)Good
E
9

Use Polly

https://github.com/App-vNext/Polly-Samples

Here is a retry-generic I use with Polly

public T Retry<T>(Func<T> action, int retryCount = 0)
{
    PolicyResult<T> policyResult = Policy
     .Handle<Exception>()
     .Retry(retryCount)
     .ExecuteAndCapture<T>(action);

    if (policyResult.Outcome == OutcomeType.Failure)
    {
        throw policyResult.FinalException;
    }

    return policyResult.Result;
}

Use it like this

var result = Retry(() => MyFunction()), 3);
Embryogeny answered 28/2, 2016 at 13:51 Comment(1)
Async version for those who need one: https://mcmap.net/q/73534/-cleanest-way-to-write-retry-logicGuyton
P
9

Keep it simple with C# 6.0

public async Task<T> Retry<T>(Func<T> action, TimeSpan retryInterval, int retryCount)
{
    try
    {
        return action();
    }
    catch when (retryCount != 0)
    {
        await Task.Delay(retryInterval);
        return await Retry(action, retryInterval, --retryCount);
    }
}
Polley answered 5/4, 2017 at 11:24 Comment(1)
I am kind of curious, would this spawn an insane amount of threads with a high retry count and interval because of returning the same awaitable method?Hachure
P
7

Building on the previous work, I thought about enhancing the retry logic in three ways:

  1. Specifying what exception type to catch/retry. This is the primary enhacement as retrying for any exception is just plain wrong.
  2. Not nesting the last try in a try/catch, achieving slightly better performance
  3. Making it an Action extension method

    static class ActionExtensions
    {
      public static void InvokeAndRetryOnException<T> (this Action action, int retries, TimeSpan retryDelay) where T : Exception
      {
        if (action == null)
          throw new ArgumentNullException("action");
    
        while( retries-- > 0 )
        {
          try
          {
            action( );
            return;
          }
          catch (T)
          {
            Thread.Sleep( retryDelay );
          }
        }
    
        action( );
      }
    }
    

The method can then be invoked like so (anonymous methods can be used as well, of course):

new Action( AMethodThatMightThrowIntermittentException )
  .InvokeAndRetryOnException<IntermittentException>( 2, TimeSpan.FromSeconds( 1 ) );
Pencel answered 16/3, 2012 at 2:4 Comment(1)
This is excellent. But personally I wouldn't call it 'retryTimeout' as it's not really a Timeout. 'RetryDelay', perhaps?Carolyn
G
5

I have two implementations of this pattern using Polly. One is async.

My synchronous method is based on this answer by Erik Bergstedt

public static T Retry<T>(Func<T> action, TimeSpan retryWait, int retryCount = 0)
{
    PolicyResult<T> policyResult = Policy
        .Handle<ApiException>(ex => ex.ResponseCode == (int)HttpStatusCode.TooManyRequests)
        .WaitAndRetry(retryCount, retryAttempt => retryWait)
        .ExecuteAndCapture(action);

    if (policyResult.Outcome == OutcomeType.Failure)
    {
        throw policyResult.FinalException;
    }

    return policyResult.Result;
}

Async:

public static async Task<T> RetryAsync<T>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 0)
{
    PolicyResult<T> policyResult = await Policy
        .Handle<ApiException>(ex => ex.ResponseCode == (int)HttpStatusCode.TooManyRequests)
        .WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
        .ExecuteAndCaptureAsync(action);

    if (policyResult.Outcome == OutcomeType.Failure)
    {
        throw policyResult.FinalException;
    }

    return policyResult.Result;
}

It would also be easy to allow an exception type to be passed in as well as the lambda for the exception type.

Guyton answered 17/6, 2021 at 4:47 Comment(0)
S
4

I'd implement this:

public static bool Retry(int maxRetries, Func<bool, bool> method)
{
    while (maxRetries > 0)
    {
        if (method(maxRetries == 1))
        {
            return true;
        }
        maxRetries--;
    }
    return false;        
}

I wouldn't use exceptions the way they're used in the other examples. It seems to me that if we're expecting the possibility that a method won't succeed, its failure isn't an exception. So the method I'm calling should return true if it succeeded, and false if it failed.

Why is it a Func<bool, bool> and not just a Func<bool>? So that if I want a method to be able to throw an exception on failure, I have a way of informing it that this is the last try.

So I might use it with code like:

Retry(5, delegate(bool lastIteration)
   {
       // do stuff
       if (!succeeded && lastIteration)
       {
          throw new InvalidOperationException(...)
       }
       return succeeded;
   });

or

if (!Retry(5, delegate(bool lastIteration)
   {
       // do stuff
       return succeeded;
   }))
{
   Console.WriteLine("Well, that didn't work.");
}

If passing a parameter that the method doesn't use proves to be awkward, it's trivial to implement an overload of Retry that just takes a Func<bool> as well.

Slapdash answered 13/10, 2009 at 22:56 Comment(5)
+1 for avoiding the exception. Though I'd do a void Retry(...) and throw something? Boolean returns and/or return codes are too often overlooked.Novercal
"if we're expecting the possibility that a method won't succeed, its failure isn't an exception" - while that's true in some cases, exception need not imply exceptional. It's for error handling. There is no guarantee that the caller will check a Boolean result. There is a guarantee that an exception will be handled (by the runtime shutting down the application if nothing else does).Surgeon
I can't find the reference but I believe .NET defines an Exception as "a method didn't do what it said it will do". 1 purpose is to use exceptions to indicate a problem rather than the Win32 pattern of requiring the caller to check the return value if the function succeeded or not.Jene
But exceptions don't merely "indicate a problem." They also include a mass of diagnostic information that costs time and memory to compile. There are clearly situations in which that doesn't matter the least little bit. But there are a lot where it does. .NET doesn't use exceptions for control flow (compare, say, with Python's use of the StopIteration exception), and there's a reason.Slapdash
The TryDo method pattern is a slippery slope. Before you know it, your entire call stack will consist of TryDo methods. Exceptions were invented to avoid such a mess.Bechuana
M
4

Update after 6 years: now I consider that the approach below is pretty bad. To create a retry logic we should consider to use a library like Polly.


My async implementation of the retry method:

public static async Task<T> DoAsync<T>(Func<dynamic> action, TimeSpan retryInterval, int retryCount = 3)
    {
        var exceptions = new List<Exception>();

        for (int retry = 0; retry < retryCount; retry++)
        {
            try
            {
                return await action().ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }

            await Task.Delay(retryInterval).ConfigureAwait(false);
        }
        throw new AggregateException(exceptions);
    }

Key points: I used .ConfigureAwait(false); and Func<dynamic> instead Func<T>

Marlanamarlane answered 7/3, 2014 at 0:41 Comment(4)
This does not provide an answer to the question. Please consider posting your answer as a new question, using the "Ask Question" button at the top of the page, then posting your own answer to the question to share what you learned with the community.Mechanician
Much simpler with C# 5.0 than codereview.stackexchange.com/q/55983/54000 but maybe CansellactionToken should be injected.Crescendo
There's a problem with this implementation. After the final retry, right before giving up, Task.Delay is called for no reason.Bechuana
@Bechuana this is a 6 years old answer and now I consider that its a pretty bad approach to create a retry logic :)) thanks for notification. I will update my answer according to that consideration.Marlanamarlane
P
3

I needed a method that supports cancellation, while I was at it, I added support for returning intermediate failures.

public static class ThreadUtils
{
    public static RetryResult Retry(
        Action target,
        CancellationToken cancellationToken,
        int timeout = 5000,
        int retries = 0)
    {
        CheckRetryParameters(timeout, retries)
        var failures = new List<Exception>();
        while(!cancellationToken.IsCancellationRequested)
        {
            try
            {
                target();
                return new RetryResult(failures);
            }
            catch (Exception ex)
            {
                failures.Add(ex);
            }

            if (retries > 0)
            {
                retries--;
                if (retries == 0)
                {
                    throw new AggregateException(
                     "Retry limit reached, see InnerExceptions for details.",
                     failures);
                }
            }

            if (cancellationToken.WaitHandle.WaitOne(timeout))
            {
                break;
            }
        }

        failures.Add(new OperationCancelledException(
            "The Retry Operation was cancelled."));
        throw new AggregateException("Retry was cancelled.", failures);
    }

    private static void CheckRetryParameters(int timeout, int retries)
    {
        if (timeout < 1)
        {
            throw new ArgumentOutOfRangeException(...
        }

        if (retries < 0)
        {
            throw new ArgumentOutOfRangeException(...

        }
    }

    public class RetryResult : IEnumerable<Exception>
    {
        private readonly IEnumerable<Exception> failureExceptions;
        private readonly int failureCount;

         protected internal RetryResult(
             ICollection<Exception> failureExceptions)
         {
             this.failureExceptions = failureExceptions;
             this.failureCount = failureExceptions.Count;
         }
    }

    public int FailureCount
    {
        get { return this.failureCount; }
    }

    public IEnumerator<Exception> GetEnumerator()
    {
        return this.failureExceptions.GetEnumerator();
    }

    System.Collections.IEnumerator 
        System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

You can use the Retry function like this, retry 3 times with a 10 second delay but without cancellation.

try
{
    var result = ThreadUtils.Retry(
        SomeAction, 
        CancellationToken.None,
        10000,
        3);

    // it worked
    result.FailureCount // but failed this many times first.
}
catch (AggregationException ex)
{
   // oops, 3 retries wasn't enough.
}

Or, retry eternally every five seconds, unless cancelled.

try
{
    var result = ThreadUtils.Retry(
        SomeAction, 
        someTokenSource.Token);

    // it worked
    result.FailureCount // but failed this many times first.
}
catch (AggregationException ex)
{
   // operation was cancelled before success.
}

As you can guess, In my source code I've overloaded the Retry function to support the differing delgate types I desire to use.

Persist answered 27/9, 2012 at 15:8 Comment(0)
P
3

This method allows retries on certain exception types (throws others immediately).

public static void DoRetry(
    List<Type> retryOnExceptionTypes,
    Action actionToTry,
    int retryCount = 5,
    int msWaitBeforeEachRety = 300)
{
    for (var i = 0; i < retryCount; ++i)
    {
        try
        {
            actionToTry();
            break;
        }
        catch (Exception ex)
        {
            // Retries exceeded
            // Throws on last iteration of loop
            if (i == retryCount - 1) throw;

            // Is type retryable?
            var exceptionType = ex.GetType();
            if (!retryOnExceptionTypes.Contains(exceptionType))
            {
                throw;
            }

            // Wait before retry
            Thread.Sleep(msWaitBeforeEachRety);
        }
    }
}
public static void DoRetry(
    Type retryOnExceptionType,
    Action actionToTry,
    int retryCount = 5,
    int msWaitBeforeEachRety = 300)
        => DoRetry(new List<Type> {retryOnExceptionType}, actionToTry, retryCount, msWaitBeforeEachRety);

Example usage:

DoRetry(typeof(IOException), () => {
    using (var fs = new FileStream(requestedFilePath, FileMode.Create, FileAccess.Write))
    {
        fs.Write(entryBytes, 0, entryBytes.Length);
    }
});
Planchet answered 23/8, 2019 at 10:11 Comment(0)
T
2

Exponential backoff is a good retry strategy than simply trying x number of times. You can use a library like Polly to implement it.

Tousle answered 21/12, 2016 at 21:58 Comment(2)
Polly sample using it ?Inalterable
@Inalterable Here's a Polly implementation. https://mcmap.net/q/73534/-cleanest-way-to-write-retry-logicGuyton
O
2

For those who want to have both the option to retry on any exception or explicitly set the exception type, use this:

public class RetryManager 
{
    public void Do(Action action, 
                    TimeSpan interval, 
                    int retries = 3)
    {
        Try<object, Exception>(() => {
            action();
            return null;
        }, interval, retries);
    }

    public T Do<T>(Func<T> action, 
                    TimeSpan interval, 
                    int retries = 3)
    {
        return Try<T, Exception>(
              action
            , interval
            , retries);
    }

    public T Do<E, T>(Func<T> action, 
                       TimeSpan interval, 
                       int retries = 3) where E : Exception
    {
        return Try<T, E>(
              action
            , interval
            , retries);
    }

    public void Do<E>(Action action, 
                       TimeSpan interval, 
                       int retries = 3) where E : Exception
    {
        Try<object, E>(() => {
            action();
            return null;
        }, interval, retries);
    }

    private T Try<T, E>(Func<T> action, 
                       TimeSpan interval, 
                       int retries = 3) where E : Exception
    {
        var exceptions = new List<E>();

        for (int retry = 0; retry < retries; retry++)
        {
            try
            {
                if (retry > 0)
                    Thread.Sleep(interval);
                return action();
            }
            catch (E ex)
            {
                exceptions.Add(ex);
            }
        }

        throw new AggregateException(exceptions);
    }
}
Octillion answered 23/1, 2017 at 6:54 Comment(0)
B
1

Here's an async/await version that aggregates exceptions and supports cancellation.

/// <seealso href="https://learn.microsoft.com/en-us/azure/architecture/patterns/retry"/>
protected static async Task<T> DoWithRetry<T>( Func<Task<T>> action, CancellationToken cancelToken, int maxRetries = 3 )
{
    var exceptions = new List<Exception>();

    for ( int retries = 0; !cancelToken.IsCancellationRequested; retries++ )
        try {
            return await action().ConfigureAwait( false );
        } catch ( Exception ex ) {
            exceptions.Add( ex );

            if ( retries < maxRetries )
                await Task.Delay( 500, cancelToken ).ConfigureAwait( false ); //ease up a bit
            else
                throw new AggregateException( "Retry limit reached", exceptions );
        }

    exceptions.Add( new OperationCanceledException( cancelToken ) );
    throw new AggregateException( "Retry loop was canceled", exceptions );
}
Bechuana answered 2/2, 2020 at 14:45 Comment(0)
N
0

I had the need to pass some parameter to my method to retry, and have a result value; so i need an expression.. I build up this class that does the work (it is inspired to the the LBushkin's one) you can use it like this:

static void Main(string[] args)
{
    // one shot
    var res = Retry<string>.Do(() => retryThis("try"), 4, TimeSpan.FromSeconds(2), fix);

    // delayed execute
    var retry = new Retry<string>(() => retryThis("try"), 4, TimeSpan.FromSeconds(2), fix);
    var res2 = retry.Execute();
}

static void fix()
{
    Console.WriteLine("oh, no! Fix and retry!!!");
}

static string retryThis(string tryThis)
{
    Console.WriteLine("Let's try!!!");
    throw new Exception(tryThis);
}

public class Retry<TResult>
{
    Expression<Func<TResult>> _Method;
    int _NumRetries;
    TimeSpan _RetryTimeout;
    Action _OnFailureAction;

    public Retry(Expression<Func<TResult>> method, int numRetries, TimeSpan retryTimeout, Action onFailureAction)
    {
        _Method = method;
        _NumRetries = numRetries;
        _OnFailureAction = onFailureAction;
        _RetryTimeout = retryTimeout;
    }

    public TResult Execute()
    {
        TResult result = default(TResult);
        while (_NumRetries > 0)
        {
            try
            {
                result = _Method.Compile()();
                break;
            }
            catch
            {
                _OnFailureAction();
                _NumRetries--;
                if (_NumRetries <= 0) throw; // improved to avoid silent failure
                Thread.Sleep(_RetryTimeout);
            }
        }
        return result;
    }

    public static TResult Do(Expression<Func<TResult>> method, int numRetries, TimeSpan retryTimeout, Action onFailureAction)
    {
        var retry = new Retry<TResult>(method, numRetries, retryTimeout, onFailureAction);
        return retry.Execute();
    }
}

ps. the LBushkin's solution does one more retry =D

Nearby answered 31/5, 2012 at 17:20 Comment(0)
P
0

I would add the following code to the accepted answer

public static class Retry<TException> where TException : Exception //ability to pass the exception type
    {
        //same code as the accepted answer ....

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

            for (int retry = 0; retry < retryCount; retry++)
            {
                try
                {
                    return action();
                }
                catch (TException ex) //Usage of the exception type
                {
                    exceptions.Add(ex);
                    Thread.Sleep(retryInterval);
                }
            }

            throw new AggregateException(String.Format("Failed to excecute after {0} attempt(s)", retryCount), exceptions);
        }
    }

Basically the above code is making the Retry class generic so you can pass the type of the exception you want to catch for retry.

Now use it almost in the same way but specifying the exception type

Retry<EndpointNotFoundException>.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));
Pfeffer answered 2/8, 2015 at 17:2 Comment(3)
The for loop will always execute a couple of times (based on your retryCount) even if the code in TRY CATCH loop was executed without exceptions. I would suggest to set retryCount equal to the retry var in the try loop, so the for loop wil stop going over it.Pytlik
@Pytlik I believe you're mistaken. If action doesn't throw then Do returns thus breaking away from the for loop.Bechuana
In any case, there's a problem with this implementation. After the final retry, right before giving up, Thread.Sleep is called for no reason.Bechuana
C
0

I know this answer is very old but I just wanted to comment on this because I have run into issues using these while, do, whatever statement with counters.

Over the years I have settled on a better approach I think. That is to use some sort of event aggregation like a reactive extensions "Subject" or the like. When a try fails, you simply publish an event saying the try failed, and have the aggregator function re-schedule the event. This allows you much more control over the retry without polluting the call itself with a bunch of retry loops and what not. Nor are you tying up a single thread with a bunch of thread sleeps.

Cuisse answered 20/3, 2016 at 20:16 Comment(0)
H
0

Do it simple in C#, Java or other languages:

  internal class ShouldRetryHandler {
    private static int RETRIES_MAX_NUMBER = 3;
    private static int numberTryes;

    public static bool shouldRetry() {
        var statusRetry = false;

        if (numberTryes< RETRIES_MAX_NUMBER) {
            numberTryes++;
            statusRetry = true;
            //log msg -> 'retry number' + numberTryes

        }

        else {
            statusRetry = false;
            //log msg -> 'reached retry number limit' 
        }

        return statusRetry;
    }
}

and use it in your code very simple:

 void simpleMethod(){
    //some code

    if(ShouldRetryHandler.shouldRetry()){
    //do some repetitive work
     }

    //some code    
    }

or you can use it in recursive methods:

void recursiveMethod(){
    //some code

    if(ShouldRetryHandler.shouldRetry()){
    recursiveMethod();
     }

    //some code    
    }
Hourigan answered 11/4, 2016 at 13:12 Comment(0)
F
0

It is also possible to use a recursive retry, by simply returning itself with an attempt counter when you need to retry. Then you need two extra parameters, one for the attempt counter, and one for the number of max attempts. Remember to increase the attempt counter upon retry. If you use an interface, you can choose to add the parameters to the interface, or add a wrapper method that calls the method with those parameters. Here is an example of the latter.

public interface ISomeService
{
  Task TryThreeTimes();
}

public class SomeService : ISomeService
{
  public async Task TryTreeTimes() {
    return await TryTreeTimes(attempt = 0, maxAttempt = 3);
  }

  private void TryTreeTimes(int attempt = 0, int maxAttempt = 3)
  {
    try
    {
      // execute code
    }
    catch (Exception exception)
    {
      // handle exception

      var needToRetry = /*some condition*/ true;
      if (needToRetry && attempt < maxAttempt)
      {
        await Task.Delay(1000);
        return TryTreeTimes(attempt + 1, maxAttempt);
      }
    }
  }
}

Make sure to handle any disposable variables properly to avoid memory leak, for instance with a finally block at the end. Remember that this will recursively add data to the stack, so be advised to balance memory usage with max possible attempts.

Fou answered 7/3 at 12:43 Comment(0)
F
-1

Or how about doing it a bit neater....

int retries = 3;
while (retries > 0)
{
  if (DoSomething())
  {
    retries = 0;
  }
  else
  {
    retries--;
  }
}

I believe throwing exceptions should generally be avoided as a mechanism unless your a passing them between boundaries (such as building a library other people can use). Why not just have the DoSomething() command return true if it was successful and false otherwise?

EDIT: And this can be encapsulated inside a function like others have suggested as well. Only problem is if you are not writing the DoSomething() function yourself

Frederic answered 13/10, 2009 at 22:50 Comment(1)
"I believe throwing exceptions should generally be avoided as a mechanism unless your a passing them between boundaries" - I completely disagree. How do you know the caller checked your false (or worse, null) return? WHY did the code fail? False tells you nothing else. What if the caller has to pass the failure up the stack? Read msdn.microsoft.com/en-us/library/ms229014.aspx - these are for libraries, but they make just as much sense for internal code. And on a team, other people are likely to call your code.Surgeon
S
-1
int retries = 3;
while (true)
{
    try
    {
        //Do Somthing
        break;
    }
    catch (Exception ex)
    {
        if (--retries == 0)
            return Request.BadRequest(ApiUtil.GenerateRequestResponse(false, "3 Times tried it failed do to : " + ex.Message, new JObject()));
        else
            System.Threading.Thread.Sleep(100);
    }
Semiweekly answered 20/12, 2016 at 8:33 Comment(1)
What do you do with Request.BadRequest?Unbeknown
B
-2
public delegate void ThingToTryDeletage();

public static void TryNTimes(ThingToTryDelegate, int N, int sleepTime)
{
   while(true)
   {
      try
      {
        ThingToTryDelegate();
      } catch {

            if( --N == 0) throw;
          else Thread.Sleep(time);          
      }
}
Barth answered 13/10, 2009 at 22:7 Comment(1)
Because the throw; is the only way the infinite loop is terminated, this method is actually implementing "try until it fails N times" and not the desired "try up to N times until it succeeds". You need a break; or return; after the call to ThingToTryDelegate(); otherwise it'll be called continuously if it never fails. Also, this won't compile because the first parameter of TryNTimes has no name. -1.Koster
T
-2

I've written a small class based on answers posted here. Hopefully it will help someone: https://github.com/natenho/resiliency

using System;
using System.Threading;

/// <summary>
/// Classe utilitária para suporte a resiliência
/// </summary>
public sealed class Resiliency
{
    /// <summary>
    /// Define o valor padrão de número de tentativas
    /// </summary>
    public static int DefaultRetryCount { get; set; }

    /// <summary>
    /// Define o valor padrão (em segundos) de tempo de espera entre tentativas
    /// </summary>
    public static int DefaultRetryTimeout { get; set; }

    /// <summary>
    /// Inicia a parte estática da resiliência, com os valores padrões
    /// </summary>
    static Resiliency()
    {
        DefaultRetryCount = 3;
        DefaultRetryTimeout = 0;
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente DefaultRetryCount vezes  quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <remarks>Executa uma vez e realiza outras DefaultRetryCount tentativas em caso de exceção. Não aguarda para realizar novas tentativa.</remarks>
    public static void Try(Action action)
    {
        Try<Exception>(action, DefaultRetryCount, TimeSpan.FromMilliseconds(DefaultRetryTimeout), null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount">Número de novas tentativas a serem realizadas</param>
    /// <param name="retryTimeout">Tempo de espera antes de cada nova tentativa</param>
    public static void Try(Action action, int retryCount, TimeSpan retryTimeout)
    {
        Try<Exception>(action, retryCount, retryTimeout, null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount">Número de novas tentativas a serem realizadas</param>
    /// <param name="retryTimeout">Tempo de espera antes de cada nova tentativa</param>
    /// <param name="tryHandler">Permitindo manipular os critérios para realizar as tentativas</param>
    public static void Try(Action action, int retryCount, TimeSpan retryTimeout, Action<ResiliencyTryHandler<Exception>> tryHandler)
    {
        Try<Exception>(action, retryCount, retryTimeout, tryHandler);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente por até DefaultRetryCount vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="tryHandler">Permitindo manipular os critérios para realizar as tentativas</param>
    /// <remarks>Executa uma vez e realiza outras DefaultRetryCount tentativas em caso de exceção. Aguarda DefaultRetryTimeout segundos antes de realizar nova tentativa.</remarks>
    public static void Try(Action action, Action<ResiliencyTryHandler<Exception>> tryHandler)
    {
        Try<Exception>(action, DefaultRetryCount, TimeSpan.FromSeconds(DefaultRetryTimeout), null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="TException"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <remarks>Executa uma vez e realiza outras DefaultRetryCount tentativas em caso de exceção. Aguarda DefaultRetryTimeout segundos antes de realizar nova tentativa.</remarks>
    public static void Try<TException>(Action action) where TException : Exception
    {
        Try<TException>(action, DefaultRetryCount, TimeSpan.FromSeconds(DefaultRetryTimeout), null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="TException"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount"></param>
    public static void Try<TException>(Action action, int retryCount) where TException : Exception
    {
        Try<TException>(action, retryCount, TimeSpan.FromSeconds(DefaultRetryTimeout), null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount"></param>
    /// <param name="retryTimeout"></param>
    public static void Try<TException>(Action action, int retryCount, TimeSpan retryTimeout) where TException : Exception
    {
        Try<TException>(action, retryCount, retryTimeout, null);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada qualquer <see cref="Exception"/> 
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="tryHandler">Permitindo manipular os critérios para realizar as tentativas</param>
    /// <remarks>Executa uma vez e realiza outras DefaultRetryCount tentativas em caso de exceção. Aguarda DefaultRetryTimeout segundos antes de realizar nova tentativa.</remarks>
    public static void Try<TException>(Action action, Action<ResiliencyTryHandler<TException>> tryHandler) where TException : Exception
    {
        Try(action, DefaultRetryCount, TimeSpan.FromSeconds(DefaultRetryTimeout), tryHandler);
    }

    /// <summary>
    /// Executa uma <see cref="Action"/> e tenta novamente determinado número de vezes quando for disparada uma <see cref="Exception"/> definida no tipo genérico
    /// </summary>
    /// <param name="action">Ação a ser realizada</param>
    /// <param name="retryCount">Número de novas tentativas a serem realizadas</param>
    /// <param name="retryTimeout">Tempo de espera antes de cada nova tentativa</param>
    /// <param name="tryHandler">Permitindo manipular os critérios para realizar as tentativas</param>
    /// <remarks>Construído a partir de várias ideias no post <seealso cref="http://stackoverflow.com/questions/156DefaultRetryCount191/c-sharp-cleanest-way-to-write-retry-logic"/></remarks>
    public static void Try<TException>(Action action, int retryCount, TimeSpan retryTimeout, Action<ResiliencyTryHandler<TException>> tryHandler) where TException : Exception
    {
        if (action == null)
            throw new ArgumentNullException(nameof(action));

        while (retryCount-- > 0)
        {
            try
            {
                action();
                return;
            }
            catch (TException ex)
            {
                //Executa o manipulador de exception
                if (tryHandler != null)
                {
                    var callback = new ResiliencyTryHandler<TException>(ex, retryCount);
                    tryHandler(callback);
                    //A propriedade que aborta pode ser alterada pelo cliente
                    if (callback.AbortRetry)
                        throw;
                }

                //Aguarda o tempo especificado antes de tentar novamente
                Thread.Sleep(retryTimeout);
            }
        }

        //Na última tentativa, qualquer exception será lançada de volta ao chamador
        action();
    }

}

/// <summary>
/// Permite manipular o evento de cada tentativa da classe de <see cref="Resiliency"/>
/// </summary>
public class ResiliencyTryHandler<TException> where TException : Exception
{
    #region Properties

    /// <summary>
    /// Opção para abortar o ciclo de tentativas
    /// </summary>
    public bool AbortRetry { get; set; }

    /// <summary>
    /// <see cref="Exception"/> a ser tratada
    /// </summary>
    public TException Exception { get; private set; }

    /// <summary>
    /// Identifca o número da tentativa atual
    /// </summary>
    public int CurrentTry { get; private set; }

    #endregion

    #region Constructors

    /// <summary>
    /// Instancia um manipulador de tentativa. É utilizado internamente
    /// por <see cref="Resiliency"/> para permitir que o cliente altere o
    /// comportamento do ciclo de tentativas
    /// </summary>
    public ResiliencyTryHandler(TException exception, int currentTry)
    {
        Exception = exception;
        CurrentTry = currentTry;
    }

    #endregion

}
Transformism answered 9/2, 2017 at 13:22 Comment(0)
B
-2

Retry helper: a generic java implementation that contains both returnable and void type retries.

import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RetryHelper {
  private static final Logger log = LoggerFactory.getLogger(RetryHelper.class);
  private int retryWaitInMS;
  private int maxRetries;

  public RetryHelper() {
    this.retryWaitInMS = 300;
    this.maxRetries = 3;
  }

  public RetryHelper(int maxRetry) {
    this.maxRetries = maxRetry;
    this.retryWaitInMS = 300;
  }

  public RetryHelper(int retryWaitInSeconds, int maxRetry) {
    this.retryWaitInMS = retryWaitInSeconds;
    this.maxRetries = maxRetry;
  }

  public <T> T retryAndReturn(Supplier<T> supplier) {
    try {
      return supplier.get();
    } catch (Exception var3) {
      return this.retrySupplier(supplier);
    }
  }

  public void retry(Runnable runnable) {
    try {
      runnable.run();
    } catch (Exception var3) {
      this.retrySupplier(() -> {
        runnable.run();
        return null;
      });
    }

  }

  private <T> T retrySupplier(Supplier<T> supplier) {
    log.error("Failed <TASK>, will be retried " + this.maxRetries + " times.");
    int retryCounter = 0;

    while(retryCounter < this.maxRetries) {
      try {
        return supplier.get();
      } catch (Exception var6) {
        ++retryCounter;
        log.error("<TASK> failed on retry: " + retryCounter + " of " + this.maxRetries + " with error: " + var6.getMessage());
        if (retryCounter >= this.maxRetries) {
          log.error("Max retries exceeded.");
          throw var6;
        }

        try {
          Thread.sleep((long)this.retryWaitInMS);
        } catch (InterruptedException var5) {
          var5.printStackTrace();
        }
      }
    }

    return supplier.get();
  }

  public int getRetryWaitInMS() {
    return this.retryWaitInMS;
  }

  public int getMaxRetries() {
    return this.maxRetries;
  }
}

Usage:

    try {
      returnValue = new RetryHelper().retryAndReturn(() -> performSomeTask(args));
      //or no return type:
      new RetryHelper().retry(() -> mytask(args));
    } catch(Exception ex){
      log.error(e.getMessage());
      throw new CustomException();
    }
Batavia answered 16/7, 2019 at 18:23 Comment(1)
This is a C# question. Your answer would be a lot more likely to be found useful on a question tagged with Java.Guyton
R
-2

I've implemented an async version of the accepted answer like so - and it seems to work nicely - any comments?


        public static async Task DoAsync(
            Action action,
            TimeSpan retryInterval,
            int maxAttemptCount = 3)
        {
            DoAsync<object>(() =>
            {
                action();
                return null;
            }, retryInterval, maxAttemptCount);
        }

        public static async Task<T> DoAsync<T>(
            Func<Task<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 await action();
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                }
            }
            throw new AggregateException(exceptions);
        }

And, call it simply like this:

var result = await Retry.DoAsync(() => MyAsyncMethod(), TimeSpan.FromSeconds(5), 4);
Ringlet answered 13/5, 2020 at 15:32 Comment(1)
Thread.Sleep? Blocking a thread negates the benefits of asynchrony. Also I am pretty sure that the Task DoAsync() version should accept an argument of type Func<Task>.Hilary

© 2022 - 2024 — McMap. All rights reserved.