How can I improve this exception retry scenario?

前端 未结 10 1511
暗喜
暗喜 2020-12-13 19:39

I have a web service method I am calling which is 3rd party and outside of my domain. For some reason every now and again the web service fails with a gateway timeout. Its i

相关标签:
10条回答
  • 2020-12-13 20:10

    All of the answers so far assume that the reaction to any exception should be to retry the operation. This is a good assumption right up until it's a false assumption. You could easily be retrying an operation that is damaging your system, all because you didn't check the exception type.

    You should almost never use a bare "catch", nor "catch (Exception ex). Catch a more-specific exception - one you know you can safely recover from.

    0 讨论(0)
  • 2020-12-13 20:11

    You should use recursion (or a loop), and should only retry if you got the error you expected.

    For example:

    static void TryExecute<TException>(Action method, Func<TException, bool> retryFilter, int maxRetries) where TException : Exception {
        try {
            method();
        } catch(TException ex) {
            if (maxRetries > 0 && retryFilter(ex))
                TryExecute(method, retryFilter, maxRetries - 1);
            else
                throw;
        }
    }
    

    EDIT: With a loop:

    static void TryExecute<TException>(Action method, Func<TException, bool> retryFilter, int maxRetries) where TException : Exception {
        while (true) {
            try {
                method();
                return;
            } catch(TException ex) {
                if (maxRetries > 0 && retryFilter(ex))
                    maxRetries--;
                else
                    throw;
            }
        }
    }
    

    You can try to prevent future errors in retryFilter, perhaps by Thread.Sleep.

    If the last retry fails, this will throw the last exception.

    0 讨论(0)
  • 2020-12-13 20:12
    int cnt=0;
    bool cont = true;
    while (cont)
    {
         try
         {
             MDO = OperationsWebService.MessageDownload(MI);
             cont = false; 
         }
         catch (Exception ex) 
         { 
             ++cnt;
             if (cnt == 5)
             {
                 // 5 retries, ok now log and deal with the error. 
                cont = false;
             }
         } 
    }
    

    UPDATED : Fixed code based on comments.

    0 讨论(0)
  • 2020-12-13 20:19

    Here is some retry logic we are using. We don't do this a lot and I was going to pull it out and document it as our Retry Pattern/Standard. I had to wing it when I first wrote it so I came here to see if I was doing it correctly. Looks like I was. The version below is fully commented. See below that for an uncommented version.

    #region Retry logic for SomeWebService.MyMethod
    // The following code wraps SomeWebService.MyMethod in retry logic
    // in an attempt to account for network failures, timeouts, etc.
    
    // Declare the return object for SomeWebService.MyMethod outside of
    // the following for{} and try{} code so that we have it afterwards.
    MyMethodResult result = null;
    
    // This logic will attempt to retry the call to SomeWebService.MyMethod
    for (int retryAttempt = 1; retryAttempt <= Config.MaxRetryAttempts; retryAttempt++)
    {
        try
        {
            result = SomeWebService.MyMethod(myId);
    
            // If we didn't get an exception, then that (most likely) means that the
            // call was successful so we can break out of the retry logic.
            break;
        }
        catch (Exception ex)
        {
            // Ideally we want to only catch and act on specific
            // exceptions related to the failure. However, in our
            // testing, we found that the exception could be any type
            // (service unavailable, timeout, database failure, etc.)
            // and attempting to trap every exception that was retryable
            // was burdensome. It was easier to just retry everything
            // regardless of the cause of the exception. YMMV. Do what is
            // appropriate for your scenario.
    
            // Need to check to see if there will be another retry attempt allowed.
            if (retryAttempt < Config.MaxRetryAttempts)
            {
                // Log that we are re-trying
                Logger.LogEvent(string.Format("Retry attempt #{0} for SomeWebService.MyMethod({1})", retryAttempt, myId);
    
                // Put the thread to sleep. Rather than using a straight time value for each
                // iteration, we are going to multiply the sleep time by how many times we
                // have currently tried to call the method. This will allow for an easy way to
                // cover a broader range of time without having to use higher retry counts or timeouts.
                // For example, if MaxRetryAttempts = 10 and RetrySleepSeconds = 60, the coverage will
                // be as follows:
                // - Retry #1 - Sleep for 1 minute
                // - Retry #2 - Sleep for 2 minutes (covering three minutes total)
                // - Retry #10 - Sleep for 10 minutes (and will have covered almost an hour of downtime)
                Thread.Sleep(retryAttempt * Config.RetrySleepSeconds * 1000);
            }
            else
            {
                // If we made it here, we have tried to call the method several
                // times without any luck. Time to give up and move on.
    
                // Moving on could either mean:
                // A) Logging the exception and moving on to the next item.
                Logger.LogError(string.Format("Max Retry Attempts Exceeded for SomeWebService.MyMethod({0})", MyId), ex);
                // B) Throwing the exception for the program to deal with.
                throw new Exception(string.Format("Max Retry Attempts Exceeded for SomeWebService.MyMethod({0})", myId), ex);
                // Or both. Your code, your call.
            }
        }
    }
    #endregion
    

    I like Samuel Neff's example of using an exception variable to see if it completely failed or not. That would have made some of the evaluations in my logic a little simpler. I could go either way. Not sure that either way has a significant advantage over the other. However, at this point in time, I'm not going to change how we do it. The important thing is to document what you are doing and why so that some idiot doesn't come through behind you and muck with everything.

    Just for kicks though, to get a better idea if the code is any shorter or cleaner one way or the other, I pulled out all the comments. They came out exactly the same number of lines. I went ahead and compiled the two versions and ran them through Reflector Code Metrics and got the following:

    Metric: Inside-Catch / Outside-For
    CodeSize: 197 / 185
    CyclomaticComplexity: 3 / 3
    Instructions: 79 / 80
    Locals: 6 / 7
    

    Final exception logic inside the catch (22 lines):

    MyMethodResult result = null;
    for (int retryAttempt = 1; retryAttempt <= Config.MaxRetryAttempts; retryAttempt++)
    {
        try
        {
            result = SomeWebService.MyMethod(myId);
            break;
        }
        catch (Exception ex)
        {
            if (retryAttempt < Config.MaxRetryAttempts)
            {
                Logger.LogEvent(string.Format("Retry attempt #{0} for SomeWebService.MyMethod({1})", retryAttempt, myId);
                Thread.Sleep(retryAttempt * Config.RetrySleepSeconds * 1000);
            }
            else
            {
                Logger.LogError(string.Format("Max Retry Attempts Exceeded for SomeWebService.MyMethod({0})", MyId), ex);
                throw new Exception(string.Format("Max Retry Attempts Exceeded for SomeWebService.MyMethod({0})", myId), ex);
            }
        }
    }
    

    Final exception logic after the for-loop (22 lines):

    MyMethodResult result = null;
    Exception retryException = null;
    for (int retryAttempt = 1; retryAttempt <= Config.MaxRetryAttempts; retryAttempt++)
    {
        try
        {
            result = SomeWebService.MyMethod(myId);
            retryException = null;
            break;
        }
        catch (Exception ex)
        {
            retryException = ex;
            Logger.LogEvent(string.Format("Retry attempt #{0} for SomeWebService.MyMethod({1})", retryAttempt, myId);
            Thread.Sleep(retryAttempt * Config.RetrySleepSeconds * 1000);
        }
    }
    if (retryException != null)
    {
        Logger.LogError(string.Format("Max Retry Attempts Exceeded for SomeWebService.MyMethod({0})", MyId), ex);
        throw new Exception(string.Format("Max Retry Attempts Exceeded for SomeWebService.MyMethod({0})", myId), ex);
    }
    
    0 讨论(0)
  • 2020-12-13 20:24

    Here's another way you might try:

    // Easier to change if you decide that 5 retries isn't right for you
    Exception exceptionKeeper = null;
    for (int i = 0; i < MAX_RETRIES; ++i)
    {
        try
        {
           MDO = OperationsWebService.MessageDownload(MI);
           break;  // correct point from Joe - thanks.
        }
        catch (Exception ex)
        {
            exceptionKeeper = ex;
            // 5 retries, ok now log and deal with the error.
        }  
    }
    

    I think it documents the intent better. It's less code as well; easier to maintain.

    0 讨论(0)
  • 2020-12-13 20:24

    Try a loop, with some kind of limit:

    int retryCount = 5;
    var done = false;
    Exception error = null;
    while (!done && retryCount > 0)
    {
        try
        {
            MDO = OperationsWebService.MessageDownload(MI);
            done = true;
        }
        catch (Exception ex)
        {
            error = ex;
        }
        if (done)
            break;
    
        retryCount--;
    }
    
    0 讨论(0)
提交回复
热议问题