代码之家  ›  专栏  ›  技术社区  ›  JL. Hans Passant

如何改进此异常重试方案?

  •  22
  • JL. Hans Passant  · 技术社区  · 14 年前

    我有一个我正在调用的Web服务方法,它是第三方,在我的域之外。由于某种原因,Web服务不时会出现故障,并出现网关超时。它的间歇和失败后直接调用它都可以成功。

    现在我面临着一个编码难题,我有代码可以完成这个技巧,但是代码看起来像业余时间,如您将在下面看到的。

    这是真的很糟糕的代码,还是根据使用情况可以接受?如果不能接受,我该如何改进它?

    看的时候,请尽量保持面部挺直。

    try
    {
        MDO = OperationsWebService.MessageDownload(MI);
    }
    catch
    {
        try
        {
            MDO = OperationsWebService.MessageDownload(MI);
        }
        catch
        {
            try
            {
                MDO = OperationsWebService.MessageDownload(MI);
            }
            catch
            {
                try
                {
                    MDO = OperationsWebService.MessageDownload(MI);
                }
                catch 
                {
                    try
                    {
                        MDO = OperationsWebService.MessageDownload(MI);
                    }
                    catch (Exception ex)
                    {
                        // 5 retries, ok now log and deal with the error.
                    }
                }
            }
        }
    }
    
    10 回复  |  直到 13 年前
        1
  •  21
  •   JL. Hans Passant    14 年前

    你可以在一个循环中完成。

    Exception firstEx = null;
    for(int i=0; i<5; i++) 
    {
        try
        {
            MDO = OperationsWebService.MessageDownload(MI);
            firstEx = null;
            break; 
        }
        catch(Exception ex)
        {
            if (firstEx == null) 
            {
                firstEx = ex;
            }
            Thread.Sleep(100 * (i + 1));
        }
    }
    if (firstEx != null) 
    {
        throw new Exception("WebService call failed after 5 retries.", firstEx);
    }
    
        2
  •  12
  •   duffymo    14 年前

    您可以尝试另一种方法:

    // 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.
        }  
    }
    

    我认为它更好地记录了意图。它的代码也更少,更容易维护。

        3
  •  11
  •   John Saunders    14 年前

    到目前为止,所有的答案都假定对任何异常的反应都应该是重试该操作。这是一个很好的假设,直到它是一个错误的假设。您可以很容易地重试正在损坏系统的操作,所有这些都是因为您没有检查异常类型。

    你应该差不多 从未 “光秃秃的” catch “也不” catch (Exception ex) .捕获一个更具体的异常——您知道可以安全地从中恢复的异常。

        4
  •  2
  •   Bevan    14 年前

    尝试一个具有某种限制的循环:

    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--;
    }
    
        5
  •  1
  •   SLaks    14 年前

    您应该使用递归(或循环),并且只有在得到预期的错误时才应该重试。

    例如:

    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;
        }
    }
    

    编辑 有一个循环:

    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;
            }
        }
    }
    

    您可以尝试在 retryFilter ,也许 Thread.Sleep .

    如果最后一次重试失败,这将引发最后一个异常。

        6
  •  1
  •   mattsmith321    14 年前

    这里有一些我们正在使用的重试逻辑。我们做的不多,我将把它拉出并记录为我们的重试模式/标准。当我第一次写它的时候,我必须把它放在机翼上,所以我来这里是想看看我写的是否正确。看起来我是。下面的版本是完全注释的。有关未注释的版本,请参见下面的内容。

    #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
    

    我喜欢SamuelNeff使用异常变量来查看它是否完全失败的例子。这会使我逻辑中的一些评估变得简单一些。我可以走任何一条路。不确定这两种方法都有明显的优势。不过,在这个时候,我不会改变我们的做法。重要的是要记录下你在做什么,为什么这样一些白痴就不会从你身后走过,把所有的事情都弄糟。

    不过,为了更好地了解代码是否在某种程度上更短或更干净,我把所有的注释都删掉了。他们排出来的行数完全一样。我继续编译了这两个版本,并通过Reflector代码度量来运行它们,得到了以下结果:

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

    catch内的最终异常逻辑(22行):

    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);
            }
        }
    }
    

    for循环之后的最终异常逻辑(22行):

    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);
    }
    
        7
  •  1
  •   Martin Buberl    13 年前

    我正在为重试方案使用以下通用方法。我特别想引起大家的注意 PreserveStackTrace 方法,它有助于保留完整的调用堆栈跟踪,因为(正如我学到的困难方法)两者都没有 throw throw ex 生成完整的调用堆栈跟踪信息。

    public static void RetryBeforeThrow<T>(Action action, int retries, int timeout) where T : Exception
    {
        int tries = 1;
    
        do
        {
            try
            {
                action();
                return;
            }
            catch (T ex)
            {
                if (retries <= 0)
                {
                    PreserveStackTrace(ex);
                    throw;
                }
    
                Thread.Sleep(timeout);
            }
        }
        while (tries++ < retries);
    }
    
    /// <summary>
    /// Sets a flag on an <see cref="T:System.Exception"/> so that all the stack trace information is preserved 
    /// when the exception is re-thrown.
    /// </summary>
    /// <remarks>This is useful because "throw" removes information, such as the original stack frame.</remarks>
    /// <see href="http://weblogs.asp.net/fmarguerie/archive/2008/01/02/rethrowing-exceptions-and-preserving-the-full-call-stack-trace.aspx"/>
    public static void PreserveStackTrace(Exception ex)
    {
        MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic);
        preserveStackTrace.Invoke(ex, null);
    }
    
        8
  •  0
  •   Seth Petry-Johnson    14 年前

    正如其他人所指出的,正确的方法是用某种类型的max_重试将您的try/catch包装在某个循环中。

    您还可以考虑在每个循环迭代之间添加超时。否则,在暂时性问题有机会自行解决之前,您可能会烧毁重试计数器。

        9
  •  0
  •   moribvndvs    14 年前

    似乎你有你需要的答案,但我想我会发布这个链接, What is an Action Policy? 我发现这提供了一个更优雅的解决方案。Lokad有一些相当复杂的实现,但是这个家伙的逻辑是非常可靠的,并且您最终编写的最终代码是非常简单的。

        10
  •  0
  •   James Curran    14 年前
    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;
             }
         } 
    }
    

    更新:基于注释的固定代码。