编写重试逻辑最干净的方法?

noc*_*ura 434 .net c#

偶尔我需要在放弃之前多次重试一次手术.我的代码是这样的:

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

我想在一般的重试函数中重写它,如:

TryThreeTimes(DoSomething);
Run Code Online (Sandbox Code Playgroud)

在C#中有可能吗?该TryThreeTimes()方法的代码是什么?

LBu*_*kin 540

如果用作一般异常处理机制,那么简单地重试相同调用的Blanket catch语句可能是危险的.话虽如此,这是一个基于lambda的重试包装器,您可以使用任何方法.我选择将重试次数和重试超时作为参数进行分解,以获得更大的灵活性:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

您现在可以使用此实用程序方法执行重试逻辑:

Retry.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));
Run Code Online (Sandbox Code Playgroud)

要么:

Retry.Do(SomeFunctionThatCanFail, TimeSpan.FromSeconds(1));
Run Code Online (Sandbox Code Playgroud)

要么:

int result = Retry.Do(SomeFunctionWhichReturnsInt, TimeSpan.FromSeconds(1), 4);
Run Code Online (Sandbox Code Playgroud)

或者你甚至可以async超载.

  • 我们在大批量Biztalk应用程序中使用类似的模式进行数据库访问,但有两个改进:我们有不应重试的异常的黑名单,我们存储发生的第一个异常,并在重试最终失败时抛出异常.原因是第二个和后面的例外通常与第一个不同.在这种情况下,只重新抛出最后一个异常时隐藏了初始问题. (38认同)
  • +1,特别是警告和错误检查.如果将异常类型传入catch作为泛型参数(其中T:Exception),我会更自在. (7认同)
  • 您也可以尝试使用[Polly](https://www.nuget.org/packages/Polly/)这样的开源库来处理这个问题.在重试之间等待的灵活性要大得多,并且已经使用该项目的许多其他人验证了它.示例:````Policy.Handle <DivideByZeroException>().WaitAndRetry(new [] {TimeSpan.FromSeconds(1),TimeSpan.FromSeconds(2),TimeSpan.FromSeconds(3)});``` (6认同)
  • 我的意图是“重试”实际上意味着重试。但是将其更改为“尝试”并不太难。只要名称保持有意义。还有其他改进代码的机会,例如检查负重试或负超时 - 例如。我省略了这些主要是为了保持示例简单……但同样,在实践中,这些可能是对实现的很好的增强。 (2认同)
  • @Dexters我们抛出一个新异常,原始异常作为内部异常.原始堆栈跟踪可用作内部异常的属性. (2认同)

Mic*_*den 208

你应该试试波莉.它是我编写的.NET库,允许开发人员以流畅的方式表达瞬态异常处理策略,如重试,重试永久,等待和重试或断路器.

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

  • 你有*real*代码的例子吗? (11认同)
  • Polly示例可以在这里找到:https://github.com/App-vNext/Polly-Samples (11认同)
  • 还有一个用于制作这个绝佳的必备套餐. (10认同)
  • OnRetry 委托实际上是什么?我认为这是发生异常时我们需要执行的操作。因此,当发生异常时 OnRetry 委托将调用然后执行委托。是这样吗? (2认同)

Eri*_*ert 59

这可能是一个坏主意.首先,它是"格言疯狂的定义是两次做同样的事情并且每次都期望不同的结果"的格言的象征.其次,这种编码模式本身并不能很好地构成.例如:

假设您的网络硬件层在发生故障时重新发送三次数据包,等待,比如故障之间的第二次.

现在假设软件层在数据包故障时重新发送有关故障三次的通知.

现在假设通知层在通知传递失败时重新激活通知三次.

现在假设错误报告层在通知失败时重新激活通知层三次.

现在假设Web服务器在错误失败时重新激活错误报告三次.

现在假设Web客户端在从服务器收到错误时重新发送请求三次.

现在假设网络交换机上应该将通知路由到管理员的线路已拔下.Web客户端的用户何时最终收到错误消息?我在大约十二分钟后制作它.

以免你认为这只是一个愚蠢的例子:我们在客户代码中看到了这个错误,尽管远比我在这里描述的要糟糕得多.在特定的客户代码中,发生的错误情况与最终报告给用户之间的差距是几周,因为很多层都在自动重试等待.试想一下,如果有十次重试而不是三次重试会发生什么.

通常,对错误条件做正确的事情是立即报告并让用户决定做什么. 如果用户想要创建自动重试策略,请让他们在软件抽象中的适当级别创建该策略.

  • -1此建议对自动批处理系统遇到的瞬时网络故障毫无用处. (204认同)
  • 当您长时间运行使用网络资源的批处理作业(例如Web服务)时,您不能指望网络100%可靠.偶尔会有超时,插座断开,甚至可能发生虚假的路由故障或使用时发生的服务器中断.一种选择是失败,但这可能意味着以后重新开始冗长的工作.另一个选择是以适当的延迟重试几次以查看它是否是临时问题,然后失败.我同意你必须要注意的构图......但它有时候是最好的选择. (41认同)
  • +1.Raymond在这里分享了一个真实的例子,http://blogs.msdn.com/oldnewthing/archive/2005/11/07/489807.aspx (19认同)
  • 不确定这是不是说"不要做"然后"做".大多数提出这个问题的人都可能是从事软件抽象工作的人. (15认同)
  • 我认为你在答案开头使用的引用很有意思.如果先前的经验经常给你相同的结果,"期待不同的结果"只是精神错乱.虽然软件建立在一致性承诺的基础上,但在某些情况下我们需要与我们无法控制的不可靠力量进行交互. (14认同)
  • 实际上我认为这是非确定性的定义. (5认同)
  • @probackpacker:当然 - 问题是"在不可预测的力量可能改变的时间范围内?" 电源峰值使网络路由器在一毫秒内没有响应,有人将路由器断电一小时,这是非常不同的事情!(批评我的引用的另一种方式是,当然这不是*任何甚至*模糊地*就像疯狂的实际定义.) (3认同)
  • @probackpacker:举个例子:我*做*使用"尝试,再试一次"方法的情况是我写文件,关闭它,然后很快再打开它.(想象一下用于诊断程序崩溃不可预测的日志文件;我不知道最后一次关闭的时间是什么,我不想丢失任何数据.)这种情况非常常见.写入病毒检查程序锁定文件*时刻*它关闭然后花几毫秒检查病毒; 如果我试图在几毫秒内打开文件它将失败,但可能在第二次尝试时成功. (3认同)
  • 糟糕的建议。例如,指数退避模式是众所周知的并且通常用于大型分布式系统。以下是引用此模式的众多来源之一:https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/implement-retries-exponential-backoff,正确的答案应该指导反对这种模式,而不是简单地说“不要这样做”。 (2认同)
  • @BartoszKP:您从不相关的技术批评开始,您承认它与我在答案中提出的问题没有任何关系,现在您已经转向语气批评。我只会对技术优点的实质性批评做出进一步回应,而不是对措辞选择和你对我的“语气”的解释做出回应。我鼓励您将批评限制在一般性的实质性批评上。 (2认同)

Dre*_*kes 44

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

然后你会打电话:

TryThreeTimes(DoSomething);
Run Code Online (Sandbox Code Playgroud)

......或者......

TryThreeTimes(() => DoSomethingElse(withLocalVariable));
Run Code Online (Sandbox Code Playgroud)

更灵活的选择:

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);
        }
   }
}
Run Code Online (Sandbox Code Playgroud)

用作:

DoWithRetry(DoSomething, TimeSpan.FromSeconds(2), tryCount: 10);
Run Code Online (Sandbox Code Playgroud)

一个更现代的版本,支持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);
        }
   }
}
Run Code Online (Sandbox Code Playgroud)

用作:

await DoWithRetryAsync(DoSomethingAsync, TimeSpan.FromSeconds(2), tryCount: 10);
Run Code Online (Sandbox Code Playgroud)

  • 最好将if更改为:`--retryCount &lt;= 0`,因为如果您想通过将其设置为0来禁用重试,它将永远持续下去。从技术上讲,术语'retryCount'不是一个好名字,因为它不会如果将其设置为1,则重试。将其重命名为`tryCount`或将-放在后面。 (2认同)
  • @saille我同意。但是,OP(以及所有其他答案)都在使用`Thread.Sleep`。替代方法是使用计时器,或者如今更可能使用Task.Delay来使用async重试。 (2认同)
  • 我添加了一个异步版本. (2认同)

Gri*_*nik 32

瞬时故障处理应用程序块提供的重试策略,包括一个可扩展的集合:

  • 增加的
  • 固定间隔
  • 指数后退

它还包括一系列基于云的服务的错误检测策略.

有关更多信息,请参阅开发人员指南的本章.

可通过NuGet获取(搜索' topaz ').

  • 绝对.使用核心重试机制并提供您自己的检测策略.我们故意将这些分开.在这里找到核心nuget包:http://nuget.org/packages/TransientFaultHandling.Core (6认同)
  • 有趣的。您可以在 Windows Azure 之外使用它吗,比如在 Winforms 应用程序中? (2认同)
  • 此外,该项目现在在Apache 2.0下并接受社区贡献.http://aka.ms/entlibopen (2认同)
  • 现在已经弃用了,最后我用它包含了一些我认识不存在的错误,并且永远不会修复:https://github.com/MicrosoftArchive/transient-fault-handling-application-block. (2认同)

Bri*_*ian 15

允许函数和重试消息

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;
}
Run Code Online (Sandbox Code Playgroud)


csh*_*net 14

您还可以考虑添加要重试的异常类型.例如,这是您要重试的超时异常吗?数据库异常?

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可能还会注意到,所有其他示例在测试重试次数== 0时都存在类似问题,并且在给定负值时重试无穷大或未能引发异常.Sleep(-1000)也会在上面的catch块中失败.取决于你对人们的期待是多么"愚蠢",但防御性编程永远不会受到伤害.

  • +1,但为什么不做RetryForException <T>(...)其中T:Exception,然后catch(T e)?刚尝试过它,效果很好. (9认同)
  • 更新:实际上我一直在使用的更好的实现需要一个Predicate <Exception>委托,如果重试是合适的,则返回true.这允许您使用本机错误代码或异常的其他属性来确定重试是否适用.例如HTTP 503代码. (3认同)

Mar*_*R-L 11

我是递归和扩展方法的粉丝,所以这是我的两分钱:

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

        InvokeWithRetries(@this, --numberOfRetries);
    }
}
Run Code Online (Sandbox Code Playgroud)


Igo*_*huk 7

在前面的工作基础上,我考虑过以三种方式增强重试逻辑:

  1. 指定要捕获/重试的异常类型.这是主要的增强,因为重试任何异常都是完全错误的.
  2. 没有在try/catch中嵌套最后一次尝试,实现稍微更好的性能
  3. 使其成为一种Action扩展方法

    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( );
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)

然后可以像这样调用该方法(当然也可以使用匿名方法):

new Action( AMethodThatMightThrowIntermittentException )
  .InvokeAndRetryOnException<IntermittentException>( 2, TimeSpan.FromSeconds( 1 ) );
Run Code Online (Sandbox Code Playgroud)


Eri*_*edt 6

使用Polly

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

这是我和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;
}
Run Code Online (Sandbox Code Playgroud)

像这样使用它

var result = Retry(() => MyFunction()), 3);
Run Code Online (Sandbox Code Playgroud)


And*_*org 6

使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我很好奇,这会因为返回相同的等待方法而产生大量的重试计数和间隔很高的线程吗? (2认同)

Fab*_*ler 5

以最新方式实施LBushkin的答案:

    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);
    }  
Run Code Online (Sandbox Code Playgroud)

并使用它:

await Retry.Do([TaskFunction], retryInterval, retryAttempts);
Run Code Online (Sandbox Code Playgroud)

而函数[TaskFunction]可以是Task<T>Task


kb4*_*000 5

我使用 Polly 有两个该模式的实现。一种是异步的。

我的同步方法基于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;
}
Run Code Online (Sandbox Code Playgroud)

异步:

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;
}
Run Code Online (Sandbox Code Playgroud)

允许传入异常类型以及异常类型的 lambda 也很容易。