再試行ロジックを記述する最もクリーンな方法は?


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、2009年

13
個人的には、そのようなヘルパーメソッドには非常に注意します。ラムダを使用して実装することは確かに可能ですが、パターン自体は非常に臭いので、そのためのヘルパー(頻繁に繰り返されることを意味します)を導入すること自体が非常に疑わしく、全体的な設計が悪いことを強く示唆しています。
Pavel Minaev、2009年

12
私の場合、私のDoSomething()は、ファイルの削除やネットワークポートへのアクセスなど、リモートマシン上で処理を行っています。どちらの場合も、DoSomethingが成功するタイミングには大きなタイミングの問題があり、遠隔性のため、私が聞くことができるイベントはありません。そうそう、その臭い。提案は大歓迎です。
noctonura

13
@PavelMinaev再試行を使用すると、全体的な設計が悪いことを示唆するのはなぜですか?統合ポイントを接続する多くのコードを作成する場合、再試行の使用は間違いなく、使用を真剣に検討する必要があるパターンです。
bytedev

回答:


569

同じ呼び出しを単純に再試行する包括的なcatchステートメントは、一般的な例外処理メカニズムとして使用すると危険な場合があります。そうは言っても、これは任意のメソッドで使用できるラムダベースの再試行ラッパーです。再試行の回数と再試行のタイムアウトをパラメータとして、もう少し柔軟性を高めることを選択しました。

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、2009年

1
「再試行」は実際には再試行を意味するというのが私の意図でした。しかし、「試行」を意味するように変更することはそれほど難しくありません。名前が意味を持ち続ける限り。たとえば、負の再試行や負のタイムアウトのチェックなど、コードを改善する他の機会があります。例を単純にするために、これらをほとんど省略しましたが、実際には、これらはおそらく実装の優れた拡張機能になるでしょう。
LBushkin 2009年

40
大量のBiztalkアプリでのDBアクセスに同様のパターンを使用しますが、2つの改良点があります。再試行してはならない例外のブラックリストがあり、発生した最初の例外を保存し、最終的に再試行が失敗したときにそれをスローします。理由は、2番目以降の例外が最初の例外と異なることが多いためです。その場合、最後の例外のみを再スローすると、最初の問題が非表示になります。
TToni

2
@Dexters内​​部例外として元の例外を使用して、新しい例外をスローします。元のスタックトレースは、内部例外の属性として利用できます。
TToni 2013年

7
Pollyなどのオープンソースライブラリを使用してこれを処理することもできます。再試行間で待機する柔軟性がはるかに高く、プロジェクトを使用した他の多くの人によって検証されています。例: Policy.Handle<DivideByZeroException>().WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });
Todd Meinershagen、2015

222

あなたはポリーを試すべきです。私が書いた.NETライブラリです。開発者は、再試行、永遠に再試行、待機と再試行、サーキットブレーカーなどの一時的な例外処理ポリシーを流暢に表現できます。

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

1
実際にはOnRetryデリゲートとは何ですか?例外が発生したときに実行する必要があると思います。したがって、例外が発生すると、OnRetryデリゲートが呼び出され、その後Executeデリゲートが呼び出されます。そうですか?
user6395764

61

これはおそらく悪い考えです。まず、それは格言「狂気の定義は同じことを2回行い、毎回異なる結果を期待すること」の象徴です。第2に、このコーディングパターンはそれ自体でうまく構成されません。例えば:

ネットワークハードウェアレイヤーが、障害発生時にパケットを3回再送信し、たとえば、障害の間に1秒待機するとします。

次に、ソフトウェア層がパケット障害時に障害に関する通知を3回再送信するとします。

ここで、通知層が通知配信の失敗時に通知を3回再アクティブ化するとします。

ここで、エラー報告レイヤーが通知の失敗時に通知レイヤーを3回再アクティブ化するとします。

次に、エラーが発生したときに、Webサーバーがエラー報告を3回再アクティブ化するとします。

次に、サーバーからエラーが発生したときに、Webクライアントがリクエストを3回再送信するとします。

次に、通知を管理者にルーティングすることになっているネットワークスイッチの回線が接続されていないとします。Webクライアントのユーザーが最終的にエラーメッセージを受け取るのはいつですか?12分くらいで着きます。

これはばかげた例だと思わないでください。ここで説明したよりもはるかに悪いですが、お客様のコードにこのバグが見られます。特定の顧客コードでは、非常に多くのレイヤーが自動的に待機して再試行していたため、発生しているエラー状態と最終的にユーザーに報告されるエラー状態との間のギャップは数週間でした。3回ではなく10回の再試行があるとどうなるか想像してみてください

通常、エラー状態で正しいことは、すぐにそれを報告し、ユーザーに何をすべきかを決定させることです。 ユーザーが自動再試行のポリシーを作成する場合は、ソフトウェアアブストラクションの適切なレベルでそのポリシーを作成してもらいます。


19
+1。レイモンドは実際の例をここで共有しています、blogs.msdn.com / oldnewthing / archive / 2005/11/07 / 489807.aspx
SolutionYogi

210
-1このアドバイスは、自動バッチ処理システムで発生する一時的なネットワーク障害には役に立たない。
nohat

15
これが「やらない」の後に「やる」が続いているかどうかわからない。この質問をする人々のほとんどは、おそらくソフトウェア抽象化で働いている人々でしょう。
Jim L

44
Webサービスなどのネットワークリソースを使用するバッチジョブを長時間実行している場合、ネットワークの信頼性が100%になるとは期待できません。タイムアウト、ソケットの切断、場合によっては、誤ったルーティングの不具合、または使用中にサーバーの停止が発生する可能性があります。1つのオプションは失敗することですが、それは長いジョブを後で再開することを意味する場合があります。別のオプションは、一時的な問題であるかどうかを確認するために適切な遅延を伴って数回再試行し、その後失敗することです。私はあなたが気づかなければならない組成について同意します。しかし、それは時々最良の選択です。
Erik Funkenbusch 2013

18
回答の最初に使った引用は興味深いと思います。「異なる結果を期待しています」は、以前の経験で定期的に同じ結果が得られた場合にのみ狂気です。ソフトウェアは一貫性の約束に基づいて構築されていますが、私たちの制御の及ばない信頼できない力と相互作用する必要がある状況は確かにあります。
マイケルリチャードソン

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 <= 00に設定して再試行を無効にしたい場合、これは永遠に続くためです。技術的には1に設定すると再試行されretryCountないため、用語は実際には適切な名前ではありません。それをtryCountまたは置く-後ろ。
Stefanvds、2016年

2
@saille同意する。ただし、OP(および他のすべての回答)はを使用していThread.Sleepます。代わりに、タイマーを使用するか、最近ではasync再試行に使用する可能性が高くなりますTask.Delay
Drew Noakes、2017年

2
非同期バージョンを追加しました。
Drew Noakes、2017年

アクションの場合にのみブレークしreturns trueますか?Func<bool>
Kiquenet

32

一時障害処理アプリケーションブロックを含めて、再試行戦略の拡張可能なコレクションを提供します。

  • 増分
  • 固定間隔
  • 指数バックオフ

また、クラウドベースのサービスのエラー検出戦略のコレクションも含まれています。

詳細については、開発者ガイドのこの章を参照してください。

NuGetから入手できます(「topaz」を検索)。


1
面白い。Winformsアプリなど、Windows Azureの外部でこれを使用できますか?
マシューロック

6
もちろんです。コア再試行メカニズムを使用して、独自の検出戦略を提供します。それらを意図的に切り離しました。ここでコアnugetパッケージを見つけます:nuget.org/packages/TransientFaultHandling.Core
Grigori Melnik

2
また、プロジェクトは現在Apache 2.0の下にあり、コミュニティの貢献を受け入れています。aka.ms/entlibopen
Grigori Melnik

1
@アレックス。その一部がプラットフォームに組み込まれています。
Grigori Melnik 2015

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

retvalへの RetryMethodがTrueまたはmax retries
Kiquenet 2018

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>(...)where T:Exception、then catch(T e)をしないのですか?試してみて、完全に機能します。
TrueWill、2009年

どちらかまたはここで、Typeを使用して何もする必要がないので、単純な古いパラメーターがうまくいくと考えました。
csharptest.net

この投稿によると、@ TrueWillには明らかにcatch(T ex)にいくつかのバグがあります stackoverflow.com/questions/1577760/...
csharptest.net

3
更新:実際に私が使用してきたより良い実装は、再試行が適切である場合にtrueを返すPredicate <Exception>デリゲートを取ります。これにより、ネイティブエラーコードまたは例外の他のプロパティを使用して、再試行が適用可能かどうかを判断できます。たとえば、HTTP 503コード。
csharptest.net 2012

1
「上記のキャッチブロックでもスリープ(-1000)は失敗します」... TimeSpanを使用すると、この問題は発生しません。さらに、TimeSpanははるかに柔軟で自己記述的です。「int retryTimeout」の署名から、retryTimeoutがMS、秒、分、年であるかどうかをどのように知ることができますか?;-)
bytedev 2016

13

私は再帰と拡張の方法が好きなので、ここに2つのセントがあります。

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

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

7

前の作業に基づいて、3つの方法で再試行ロジックを強化することを考えました。

  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 2017年

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、2018

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。私は無効の再試行(...)をして何かを投げるでしょうか?ブール値の戻り値または戻りコード、あるいはその両方が見過ごされがちです。
csharptest.net 2009年

1
「メソッドが成功しない可能性を想定している場合、その失敗は例外ではありません」-場合によってはそうですが、例外は例外を意味する必要はありません。エラー処理用です。呼び出し元がブール結果をチェックするという保証はありません。そこである(何も他ません場合は、アプリケーションをシャットダウンすることで、ランタイム)例外が処理されるという保証は。
TrueWill、

参照を見つけることはできませんが、.NETは例外を「メソッドが実行すると言っていた方法で実行しなかった」と定義していると思います。1つの目的は、関数が成功したかどうかにかかわらず、呼び出し元が戻り値を確認するように要求するWin32パターンではなく、例外を使用して問題を示すことです。
noctonura、2009年

ただし、例外は単に「問題を示す」だけではありません。また、コンパイルに時間とメモリを消費する大量の診断情報も含まれています。明らかにそれが少しでも問題ではない状況があります。しかし、それができるところはたくさんあります。.NETは制御フローに例外を使用せず(たとえば、PythonによるStopIteration例外の使用と比較して)、理由があります。
Robert Rossney、2009年

TryDoメソッドパターンは、滑りやすいスロープです。気付かないうちに、コールスタック全体がTryDoメソッドで構成されます。このような混乱を避けるために例外が考案されました。
HappyNomad


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

または、キャンセルされない限り、5秒ごとに永遠に再試行します。

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想像のとおり、私のソースコードでは、使用したいさまざまなDelgate型をサポートするために関数をオーバーロードしました。


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>


これは質問に対する答えを提供しません。ページの上部にある[質問する]ボタンを使用して回答を新しい質問として投稿し、質問への独自の回答を投稿して、コミュニティで学んだことを共有することを検討してください。
エリクセニド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()コマンドtrueが成功した場合にだけコマンドを返さないのはなぜfalseですか?

編集:そしてこれは他の人が提案したように関数内にカプセル化することができます。唯一の問題は、DoSomething()自分で関数を記述していない場合です


7
「例外をスローすることは、境界を越えない限り、一般的にメカニズムとして回避されるべきだと思います」-私は完全に同意しません。呼び出し元があなたの偽の(またはもっと悪いことに、null)戻り値をチェックしたことをどのようにして知っていますか?なぜコードが失敗したのですか?Falseは他に何も教えません。呼び出し側が障害をスタックに渡す必要がある場合はどうなりますか?msdn.microsoft.com/en-us/library/ms229014.aspxをお読みください。これらはライブラリ用ですが、内部コードと同じくらい意味があります。そしてチームでは、他の人があなたのコードを呼び出す可能性があります。
TrueWill、2009年

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 2017

@scre_www間違いだと思います。actionスローしない場合は、ループから離れるDoように戻ります。breakfor
HappyNomad

いずれにしても、この実装には問題があります。最後の再試行の後、あきらめる直前Thread.Sleepに、理由もなく呼び出されます。
HappyNomad

0

私はこの答えが非常に古いことを知っていますが、私はこれらについてコメントしたかったのです。なぜなら、私はこれらのwhile、do、カウンターを使用するすべてのステートメントを使用して問題に遭遇したからです。

長年にわたって、私はより良いアプローチに落ち着きました。それは、リアクティブ拡張「Subject」などのような、ある種のイベント集約を使用することです。試行が失敗した場合は、試行が失敗したことを通知するイベントを発行し、アグリゲーター機能でイベントを再スケジュールします。これにより、一連の再試行ループで呼び出し自体を汚染することなく、再試行をはるかに制御できます。また、単一のスレッドを大量のスレッドスリープで拘束することもありません。


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 2016

0

再試行ヘルパー:戻り可能型とvoid型の両方の再試行を含む一般的な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。
BACON、2017

-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>ます。
Theodor Zoulias
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.