PG日誌

受託系 PG が C# の事を書いています

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

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

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

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

リトライのコード

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

// 戻り値のないリトライ処理
public static void Run(Action action, uint retry_count, TimeSpan retry_interval, 
    Action recovery = null, Action<Exception> error_handling = null, Action final = null)

// 戻り値があるパターンでリトライ処理
public static T Run<T>(Func<T> action, uint retry_count, TimeSpan retry_interval, 
    Action recovery = null, Action<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実行に戻ります。

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

public static class RetryContext
{
    // 戻り値のないリトライ処理
    public static void Run(Action action, uint retry_count, TimeSpan retry_interval, 
        Action recovery = null, Action<Exception> error_handling = null, Action final = null)
    {
        int i = 0;
        Exception _ex = null;

        while(true)
        {
            try
            {
                action();

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

                _ex = ex;

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

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

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

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

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

                _ex = ex;

                if (i++ >= retry_count)
                {
                    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.Run(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 void error_handling(Exception ex)
    {
        Console.WriteLine("[Error] " + ex.Message);
    }

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

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

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

さいごに

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

たぶんfinal処理は蛇足な気がしています。ラムダで外側でリソースを持っているので終わった後の後処理で片付ければいいような…

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

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