C#で汎用リトライ処理を実装する

C#でリトライの共通処理を書いてみました。

特定の操作を指定した回数分自動でリトライしてくれる仕組みになります。

VisualStudio2017 + .NET4.7 + C#7.0で書いています。(Null条件演算子使ってるので、C#6.0以降なら動くかと)

リトライのコード

戻り値なし、あり版の2種類を用意してみました。

// 戻り値のないリトライ処理
public static void Invoke
(
    Action action,           // リトライ処理を行いたい処理のデリゲート(ラムダ)
    uint retry_count,        // リトライ回数
    TimeSpan retry_interval, // リトライするまでの待機時間
    Action recovery = null,  // エラーが起きた時に実行する処理
    Func<bool, Exception> error_handling = null, // 例外発生時の処理方法
    Action final = null      // 最後に必ず呼び出す処理
);

// 戻り値があるパターンでリトライ処理
public static T Invoke<T>(Func<T> action, uint retry_count, TimeSpan retry_interval, 
    Action recovery = null, Func<bool, Exception> error_handling = null, Action final = null)

ちょっとシグネチャが長いですが、各々の引数は以下の通りになります。

Name Type Description Optional
action Action メインの処理
retry_count int リトライする回数
retry_interval TimeSpan リトライを開始するまでの待機時間
recovery Action リソースに対する回復処理 Yes
error_handling Action 例外の処理方法 Yes
final Action 成功失敗にかかわらず呼ぶfinallyの処理 Yes

まず、actionを1度実行し、失敗した場合error_handlingを呼び出します。リトライ上限に達していない場合は、retry_interval時間待機しrecoveryを実行して最初のaction実行に戻ります。

リトライ上限を超えてエラーが発生した場合、最後の例外を上位に報告します。

/// <summary>
/// 戻り値の無い処理リトライを実行します。
/// </summary>
public static void Invoke(Action action, uint retry_count, TimeSpan retry_interval,
        Action recovery = null, Func<Exception, bool> error_handling = null, Action final = null)
{
    // error_handling : エラーが発生した時のハンドル方法の指定
    //
    // T : 発生した例外
    // TResult : true : リトライして処理を継続する / false : (例外が識別済みなどの理由で)リトライせず終了

    int i = 0;
    Exception _ex = null;

    while (true)
    {
        try
        {
            action();

            break;
        }
        catch (Exception ex)
        {
            bool? proceedEx = error_handling?.Invoke(ex);

            _ex = ex;

            if (i++ >= retry_count || proceedEx == false)
            {
                throw; // 発生した例外が処理済みでもリトライを終了する
            }

            Thread.Sleep((int)retry_interval.TotalMilliseconds);

            recovery?.Invoke();
        }
        finally
        {
            final?.Invoke();
        }
    }
}

/// <summary>
/// 戻り値があるパターンのリトライ処理
/// </summary>
public static T Invoke<T>(Func<T> action, uint retry_count, TimeSpan retry_interval,
        Action recovery = null, Func<Exception, bool> error_handling = null, Action final = null)
{
    int i = 0;
    Exception _ex = null;

    while (true)
    {
        try
        {
            return action();
        }
        catch (Exception ex)
        {
            bool? proceedEx = error_handling?.Invoke(ex);

            _ex = ex;

            if (i++ >= retry_count || proceedEx == false)
            {
                throw;
            }

            Thread.Sleep((int)retry_interval.TotalMilliseconds);

            recovery?.Invoke();
        }
        finally
        {
            final?.Invoke();
        }
    }
}

使い方

実際の使い方ですが、オプション引数を全部指定した場合以下のようになります。

例として、2回リトライして3回目で処理が成功します。

internal class AppMain
{
    private static StreamWriter sw;
    private static int cnt;

    public static void Main(string[] args)
    {
        try
        {
            RetryContext.Invoke(action, 3, TimeSpan.FromMilliseconds(250), recovery, error_handling, final);
        }
        catch (Exception ex)
        {
            Console.WriteLine("[Error] " + ex.Message);
            //Console.WriteLine(ex.ToString());
        }

        Console.WriteLine("[Completed]");
        Console.ReadLine();
    }

    private static void action()
    {
        if(cnt++ < 3)
        {
            // 最初の1回とリトライ2回をエラーにする
            throw new IOException("エラー!!");
        }

        sw = new StreamWriter(@"d:\log.log");
        sw.WriteLine("Yes!");
    }

    private static void recovery()
    {
        using (sw) { }
    }

    private static bool error_handling(Exception ex)
    {
        Console.WriteLine("[Error] " + ex.Message);
        return true;
    }

    private static void final()
    {
        if (sw != null) { sw.Flush(); }
        using (sw) { }
    }
}

上記コードの実行結果は以下の通りになります。

[Error] エラー!!
[Error] エラー!!
[Error] エラー!!
[Completed]

さいごに

実行中はブロッキングで処理が実行されてしまうので、必要であればasync化してください。

finally処理は蛇足な気が少ししますが、、これラムダで外側でリソースを持っているので終わった時の後処理で片付けれてもいいかもしれません。

最近TCPやHTTP通信、FTP、RSHや外部DBへのアクセスする局面でリトライ書きまくった覚えがあるのでそういったリソースにリトライしながらアクセスする局面に使えるかと思います。

と、最後に「C#+リトライ」でGoogle検索したらQiitaに同じような記事が上がっていました。ほぼ内容同じでしたね。大体考えることは同じってことなんだと思います(汗