编写重试逻辑的最简洁方法?


455

有时我需要在放弃之前重试几次操作。我的代码是这样的:

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

我想用一般的重试功能将其重写为:

TryThreeTimes(DoSomething);

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


1
一个简单的周期还不够吗?为什么不重复执行几次逻辑呢?
Restuta

13
就个人而言,我将非常警惕任何此类辅助方法。当然可以使用lambda来实现,但是模式本身非常难闻,因此为其引入一个助手(这意味着它经常被重复使用)本身就非常可疑,并且强烈暗示着整体设计不好。
帕维尔·米纳夫

12
就我而言,我的DoSomething()正在远程计算机上执行操作,例如删除文件或尝试访问网络端口。在这两种情况下,DoSomething何时能够成功都存在主要的时序问题,并且由于远程性,我无法监听任何事件。是的,它很臭。欢迎提出建议。
noctonura

13
@PavelMinaev为什么使用重试会提示整体设计不好?如果您编写了许多连接集成点的代码,那么使用重试绝对是您应该认真考虑使用的一种模式。
bytedev '16

Answers:


569

如果简单地重试相同的调用的全栈捕获语句用作通用异常处理机制,则可能很危险。话虽如此,这是一个基于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);
    }
}

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

Retry.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));

要么:

Retry.Do(SomeFunctionThatCanFail, TimeSpan.FromSeconds(1));

要么:

int result = Retry.Do(SomeFunctionWhichReturnsInt, TimeSpan.FromSeconds(1), 4);

否则,您甚至可以async超载。


7
+1,特别是用于警告和错误检查。但是,如果将异常类型作为通用参数传递(其中T:Exception),我会感到更自在。
TrueWill

1
我的意图是“重试”实际上是指重试。但是,将其更改为“ tries”并不难。只要名称保持有意义。还有其他改进代码的机会,例如检查否定的重试或否定的超时。为了使示例简单起见,我主要省略了这些内容……但是,实际上,这些可能是对实现的良好增强。
LBushkin

40
我们在大容量的Biztalk应用程序中对数据库访问使用了类似的模式,但有两个改进:我们为不应重试的异常添加了黑名单,并存储了发生的第一个异常,并在重试最终失败时抛出该异常。原因是第二个及其后的例外通常不同于第一个例外。在那种情况下,当仅抛出最后一个异常时,您将隐藏最初的问题。
TToni 2012年

2
@Dexters我们抛出一个新异常,原始异常为内部异常。原始堆栈跟踪可用作内部异常的属性。
TToni

7
您也可以尝试使用开源库(例如Polly)来处理此问题。重试之间等待的灵活性更大,并且已被使用该项目的许多其他人所验证。示例: Policy.Handle<DivideByZeroException>().WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });
Todd Meinershagen,2015年

220

你应该试试波莉。这是我写的.NET库,允许开发人员以流畅的方式表达瞬时异常处理策略,例如Retry,Forever Retry,Wait and Retry或Circuit Breaker。

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

1
什么是OnRetry委托?我认为这是发生异常时我们需要执行的操作。因此,当发生异常时,OnRetry委托将调用,然后执行委托。是这样吗?
user6395764

60

这可能是一个坏主意。首先,它象征着“精神错乱的定义是两次做同一件事,并且每次期望得到不同的结果”的格言。其次,这种编码模式本身无法很好地组合在一起。例如:

假设您的网络硬件层在发生故障时重发了三次数据包,而在故障之间等待了一秒钟。

现在,假设软件层在数据包失败时重新发送有关失败的通知三遍。

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

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

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

现在,假设Web客户端从服务器收到错误消息后,将其重新发送请求三次。

现在,假设网络交换机上用于将通知路由到管理员的线路已拔出。Web客户端的用户何时才能收到他们的错误消息?我在大约十二分钟后到达。

免得您以为这只是一个愚蠢的例子:我们已经在客户代码中看到了这个错误,尽管比我在这里描述的要严重得多。在特定的客户代码中,错误情况发生与最终报告给用户之间的时间间隔是几周,因为这么多的层会自动等待,然后重试。试想一下如果重试10次而不是3次会发生什么

通常,对错误情况采取的正确措施是立即报告它,并让用户决定要怎么做。 如果用户要创建自动重试策略,请让他们在软件抽象的适当级别上创建该策略。


19
+1。雷蒙德在这里分享了一个真实的例子,blogs.msdn.com / oldnewthing / archive / 2005/11/07 / 489807.aspx
SolutionYogi

210
-1对于自动批处理系统遇到的瞬时网络故障,此建议无用。
nohat 2010年

15
不知道这是说“不做”,然后是“做”。提出这个问题的大多数人可能是从事软件抽象工作的人。
Jim L

44
当您长时间运行使用网络资源(例如Web服务)的批处理作业时,就不能指望网络是100%可靠的。使用时会偶尔出现超时,套接字断开连接,甚至可能是虚假的路由故障或服务器故障。一种选择是失败,但这可能意味着稍后重新启动冗长的作业。另一种选择是在适当的延迟下重试几次,以查看这是否是暂时性的问题,然后失败。我同意构图,您必须要意识到..但有时它是最佳选择。
Erik Funkenbusch

18
我认为您在回答开始时使用的引语很有趣。仅当以前的经验定期给您相同的结果时,“期望不同的结果”才是理智。尽管软件是基于一致性的保证而构建的,但是在某些情况下,我们肯定需要与无法控制的不可靠力量进行交互。
2014年

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

然后,您将调用:

TryThreeTimes(DoSomething);

...或者替代地...

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

一个更灵活的选择:

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

用作:

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

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

用作:

await DoWithRetryAsync(DoSomethingAsync, TimeSpan.FromSeconds(2), tryCount: 10);

2
最好将if更改为:--retryCount <= 0因为如果要通过将其设置为0来禁用重试,它将永远持续下去。从技术上讲,该术语retryCount并不是一个好名字,因为如果将其设置为1,它不会重试。它tryCount或将-后方。
Stefanvds

2
@saille我同意。但是,OP(以及所有其他答案)都在使用Thread.Sleep。替代方案是使用定时器,或者更可能现在使用async重试,用Task.Delay
德鲁·诺阿克斯

2
我添加了一个异步版本。
Drew Noakes

只有行动才能打破returns trueFunc<bool>
Kiquenet '18

32

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

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

它还包括针对基于云的服务的错误检测策略的集合。

有关更多信息,请参见开发人员指南的这一章

可通过NuGet获得(搜索“ topaz ”)。


1
有趣。您可以在Windows Azure之外的Winforms应用程序中使用它吗?
马修·洛克

6
绝对。使用核心重试机制并提供您自己的检测策略。我们有意地将它们分离了。在此处找到核心nuget包:nuget.org/packages/TransientFaultHandling.Core
Grigori Melnik

2
另外,该项目现在在Apache 2.0下进行,并接受社区的贡献。aka.ms/entlibopen
Grigori Melnik

1
@Alex。它的各个部分正在将其放入平台。
Grigori Melnik

2
现在已弃用,最后我使用了它,其中包含一些错误,据我所知并不是,而且永远不会得到修复:github.com/MicrosoftArchive/…
Ohad Schneider

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

RetryMethod retval是True还是max retries
Kiquenet

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

您可能还会注意到,所有其他示例在测试重试== 0时也存在类似的问题,并且在给定负值时重试无穷大或无法引发异常。另外,Sleep(-1000)将在上面的catch块中失败。取决于您对人们的期望有多“傻”,但是防御性编程永远不会受到伤害。


9
+1,但为什么不RetryForException <T>(...),其中T:异常,然后catch(T e)?刚刚尝试过,它完美地工作。
TrueWill

因为我不需要使用Type进行任何操作,所以我发现普通的旧参数可以解决问题。
csharptest.net

@TrueWill显然捕捉(T EX)根据这个帖子有一些错误stackoverflow.com/questions/1577760/...
csharptest.net

3
更新:实际上,我一直在使用的更好的实现采用Predicate <Exception>委托,如果重试适当,该委托返回true。这使您可以使用本机错误代码或异常的其他属性来确定重试是否适用。例如HTTP 503代码。
csharptest.net 2012年

1
“上面的catch块中的Sleep(-1000)也将失败” ...使用TimeSpan,您将不会遇到此问题。另外,TimeSpan更加灵活和具有自我描述性。从您的“ int retryTimeout”签名中,我如何知道retryTimeout是MS,秒,分钟,年?;-)
bytedev

13

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

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

        InvokeWithRetries(@this, --numberOfRetries);
    }
}

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

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

new Action( AMethodThatMightThrowIntermittentException )
  .InvokeAndRetryOnException<IntermittentException>( 2, TimeSpan.FromSeconds( 1 ) );

1
太好了 但就我个人而言,我不会将其称为“ retryTimeout”,因为它并不是真正的超时。也许是“ RetryDelay”?
Holf

7

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

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

6

使用波莉

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

这样使用

var result = Retry(() => MyFunction()), 3);

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

并使用它:

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

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


1
谢谢你,费边!这应该一直推到最高!
JamesHoux

1
@MarkLauter的简短答案是肯定的。;-)
Fabian Bigler

4

我会实现这一点:

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

我不会像在其他示例中那样使用异常。在我看来,如果我们期望某个方法不会成功的可能性,则它的失败也不例外。因此,如果成功,我正在调用的方法应返回true,如果失败,则应返回false。

为什么是a Func<bool, bool>而不仅仅是a Func<bool>?所以,如果我想要某个方法能够在失败时引发异常,那么我可以通知它这是最后的尝试。

因此,我可能将其与类似以下代码的代码一起使用:

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

要么

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

如果通过,该方法不使用被证明是尴尬的一个参数,它是容易实现的过载Retry,只是需要一个Func<bool>为好。


1
+1以避免异常。虽然我会做一个空的Retry(...)并抛出一些东西?布尔返回值和/或返回码经常被忽略。
csharptest.net

1
“如果我们期望某个方法不会成功的可能性,那么它的失败也不例外”-尽管在某些情况下确实如此,但例外不一定意味着例外。它用于错误处理。不能保证调用方将检查布尔结果。这里一个保证的异常将被处理(由运行时关闭,如果没有人这样做的应用程序)。
TrueWill

我找不到该引用,但我相信.NET将Exception定义为“一种方法没有执行其声明将执行的操作”。一种目的是使用异常来指示问题,而不是使用Win32模式,即要求调用者检查函数成功与否的返回值。
noctonura

但是,异常不仅仅是“表明问题”。它们还包含大量的诊断信息,这些信息花费时间和内存来进行编译。显然,在某些情况下,这无关紧要。但是有很多地方可以做到。.NET不在控制流中使用异常(例如,将其与Python的异常进行比较StopIteration),这是有原因的。
罗伯特·罗斯尼

TryDo方法图案是光滑的斜坡。在不知不觉中,您的整个调用堆栈将由TryDo方法组成。发明了例外以避免这种混乱。
HappyNomad

2

指数补偿是一种很好的重试策略,而不是简单地尝试x次。您可以使用像Polly这样的库来实现它。


2

对于那些既希望重试任何异常或明确设置异常类型的人,请使用以下命令:

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

2

我需要一种支持取消的方法,在此期间,我增加了对返回中间故障的支持。

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

您可以使用这种Retry功能,以10秒的延迟重试3次,但不取消。

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

或者,每五秒钟重试一次,除非已取消。

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

您可能会猜到,在我的源代码中,我已经重载了该Retry函数以支持我希望使用的不同代理类型。


2

此方法允许重试某些异常类型(立即引发其他异常)。

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

用法示例:

DoRetry(typeof(IOException), () => {
    using (var fs = new FileStream(requestedFilePath, FileMode.Create, FileAccess.Write))
    {
        fs.Write(entryBytes, 0, entryBytes.Length);
    }
});

1

6年后更新:现在,我认为以下方法非常糟糕。要创建重试逻辑,我们应该考虑使用像Polly这样的库。


async的重试方法的实现:

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

要点:我以前.ConfigureAwait(false);Func<dynamic>替代Func<T>


这不能为问题提供答案。请考虑使用页面顶部的“问问题”按钮将您的答案发布为新问题,然后发布您自己的问题答案以与社区分享您学到的知识。
elixenide 2014年

使用C#5.0比codereview.stackexchange.com/q/55983/54000简单得多,但也许应该注入CansellactionToken。
SerG 2014年

此实现存在问题。在最终重试之后,Task.Delay即放弃之前,将无故调用。
HappyNomad

@HappyNomad这是一个有6年历史的答案,现在我认为创建重试逻辑是一种非常糟糕的方法:))感谢您的通知。我将根据这一考虑更新我的答案。
Cihan Uygun

0

或如何使它更整洁?

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

我认为通常应避免将异常作为一种机制,除非您在边界之间传递异常(例如,建立其他人可以使用的库)。为什么不成功DoSomething()返回命令,为什么不返回?truefalse呢?

编辑:并且可以像其他人建议的那样封装在一个函数中。唯一的问题是,如果您不DoSomething()自己编写函数


7
“我认为通常应避免将异常作为一种机制,除非您在边界之间传递异常”-我完全不同意。您怎么知道呼叫者检查了您的错误(或更糟糕的是,null)回报?为什么代码失败?假告诉你什么都没有。如果调用者必须将故障传递给堆栈怎么办?阅读msdn.microsoft.com/zh-cn/library/ms229014.aspx-这些用于库,但是对于内部代码也是如此。在团队中,其他人可能会调用您的代码。
TrueWill

0

我需要将一些参数传递给我的方法以重试,并获得结果值;所以我需要一个表达式..我建立了一个完成工作的类(它受LBushkin的启发),您可以像这样使用它:

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。LBushkin解决方案再重试一次= D


0

我将以下代码添加到接受的答案中

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

基本上,以上代码使 Retry该类具有通用性,因此您可以传递要捕获以进行重试的异常类型。

现在以几乎相同的方式使用它,但是指定异常类型

Retry<EndpointNotFoundException>.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));

即使TRY CATCH循环中的代码无例外地执行,for循环也将始终执行两次(基于您的retryCount)。我建议在try循环中将retryCount设置为等于retry var,因此for循环将停止对其进行遍历。
scre_www

@scre_www我相信你错了。如果action不抛出,则Do返回,从而break远离for循环。
HappyNomad

无论如何,这种实现都有问题。在最终重试之后,Thread.Sleep即放弃之前,将无故调用。
HappyNomad

0

我知道这个答案很老,但是我只想对此发表评论,因为我在使用while,do和带有计数器的任何语句时都遇到了问题。

多年来,我认为我会采用更好的方法。那就是使用某种事件聚合,例如反应性扩展“主题”等。如果尝试失败,则只需发布一个事件,说明尝试失败,然后使聚合器函数重新安排该事件的时间。这使您可以对重试进行更多控制,而不会因一堆重试循环而污染调用本身,而不会造成重污染。您也不会用一堆线程睡眠来捆绑单个线程。


0

使用C#,Java或其他语言来简化它:

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

并在您的代码中非常简单地使用它:

 void simpleMethod(){
    //some code

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

    //some code    
    }

或者您可以在递归方法中使用它:

void recursiveMethod(){
    //some code

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

    //some code    
    }

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

你怎么办Request.BadRequest
Danh

0

重试助手:包含可返回和无效类型重试的通用Java实现。

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

用法:

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

0

这是一个async/ await版本,用于汇总异常并支持取消。

/// <seealso href="https://docs.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 );
}

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

因为throw;是无限循环终止的唯一方法,所以此方法实际上是在执行“尝试直到N次失败”,而不是实现“尝试N多次直到成功”。您需要在break;return;之后再调用,ThingToTryDelegate();否则,如果它从未失败,它将被连续调用。另外,由于第一个参数TryNTimes没有名称,因此无法编译。-1。
培根

-1

我根据此处发布的答案写了一个小班。希望它将对某人有所帮助: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

}

-1

我已经实现了接受的答案的异步版本,例如-看起来效果很好-有何评论?


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

并且,简单地这样称呼它:

var result = await Retry.DoAsync(() => MyAsyncMethod(), TimeSpan.FromSeconds(5), 4);

Thread.Sleep?阻塞线程消除了异步的好处。我也很确定Task DoAsync()版本应该接受type的参数Func<Task>
西奥多·祖里亚斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.