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

时间:2010-02-16 02:26:21

标签: c# .net exception-handling

我有一个我正在调用的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 个答案:

答案 0 :(得分:21)

你可以循环播放。

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

答案 1 :(得分:12)

这是您尝试的另一种方式:

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

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

答案 2 :(得分:11)

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

你几乎永远不会使用裸露的“catch”,也不会使用“catch (Exception ex)。抓住更具体的异常 - 你知道可以安全恢复的异常。” / p>

答案 3 :(得分:2)

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

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

答案 4 :(得分:1)

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

例如:

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 {/ 1}}来预防Thread.Sleep中的未来错误。

如果上次重试失败,则会抛出最后一个异常。

答案 5 :(得分:1)

这是我们正在使用的一些重试逻辑。我们不会这么做很多,我会把它拉出来并记录为我们的重试模式/标准。我第一次写这篇文章的时候不得不把它放在一边,所以我来到这里看看我是否正确地做到了。看起来像我。以下版本已完全注释。有关未注释的版本,请参见下文。

#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

我喜欢Samuel Neff使用异常变量来查看它是否完全失败的示例。这会使我的逻辑中的一些评估变得更简单一些。我可以去任何一个方向。不确定这两种方式都有明显优势。但是,在这个时间点,我不会改变我们的方式。重要的是要记录你正在做的事情,以及为什么有些白痴不会在你身后出现并且捣乱一切。

尽管如此,为了更好地了解代码是否更短或更清洁,我提出了所有评论。它们出现的线数完全相同。我继续编译了两个版本并通过Reflector Code Metrics运行它们并获得以下内容:

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

答案 6 :(得分:1)

我正在使用以下通用方法进行重试方案。我特别想提请注意有助于保留完整调用堆栈跟踪的PreserveStackTrace方法,因为(因为我学到了很难),throwthrow 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);
}

答案 7 :(得分:0)

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

更新:修正了基于评论的代码。

答案 8 :(得分:0)

正如其他人都指出的那样,正确的方法是使用某种MAX_RETRY将try / catch包装在某个循环中。

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

答案 9 :(得分:0)

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