C#でリトライ処理を共通化してみた

ニーズがあるかどうかは別としてリトライ処理の共通クラスを作成してみました。

作成したクラスはリトライのルールを定義するクラスと実際にリトライを行うクラスです。

```csharp:RetryPolicyBase
/// <summary>
/// リトライ処理が必要な場合の、リトライのルールを定義する抽象クラスです。
/// このクラスを継承してリトライが必要な各処理でルールを定義します。
/// </summary>
public abstract class RetryPolicyBase
{
    /// <summary>
    /// 最大リトライ回数
    /// </summary>
    public abstract int MaxRetryNum { get; }

    /// <summary>
    /// リトライ時のベースとなる処理停止時間
    /// </summary>
    public abstract int RetrySleep { get; }

    /// <summary>
    /// リトライ時の最大処理停止時間
    /// </summary>
    public abstract int MaxSleep { get; }

    /// <summary>
    /// リトライ時の最小処理停止時間
    /// </summary>
    public abstract int MinSleep { get; }
}

/// <summary>
/// リトライ処理が必要な箇所で指定された条件によってリトライを行うためのクラスです。
/// </summary>
public class RetryExecutor
{
    private readonly RetryPolicyBase _policy;

    /// <summary>
    /// リトライルールを受け取ってインスタンスを生成します。
    /// </summary>
    /// <param name="policy"></param>
    public RetryExecutor(RetryPolicyBase policy)
    {
        _policy = policy;
    }

    /// <summary>
    /// リトライをかけながら指定された処理を実行します。
    /// </summary>
    /// <param name="executeAction">実行する処理</param>
    /// <param name="finallyAction">全てのリトライが完了した後に行う処理</param>
    public virtual void Execute(Action executeAction, Action finallyAction = null)
    {
        for (var retryNum = 0; retryNum < _policy.MaxRetryNum; retryNum++)
        {
            try
            {
                executeAction();
            }
            catch (Exception)
            {
                if (retryNum >= _policy.MaxRetryNum)
                    throw;

                var sleep = (int)Math.Pow(retryNum + 1, 2.0) + _policy.RetrySleep;
                sleep = (_policy.MaxSleep < sleep)
                    ? _policy.MaxSleep
                    : (sleep < _policy.MinSleep) ? _policy.MinSleep : sleep;

                Thread.Sleep(sleep);
            }
        }

        if (finallyAction != null)
            finallyAction();
    }

    /// <summary>
    /// リトライをかけながら指定された処理を実行します。
    /// </summary>
    /// <param name="executeAction">実行する処理</param>
    /// <param name="errorAction">処理実行時に発生したエラー処理</param>
    /// <param name="finallyAction">全てのリトライが完了した後に行う処理</param>
    public virtual void Execute(Action executeAction, Action<Exception> errorAction, Action finallyAction = null)
    {
        for (var retryNum = 0; retryNum < _policy.MaxRetryNum; retryNum++)
        {
            try
            {
                executeAction();
            }
            catch (Exception ex)
            {
                errorAction(ex);

                if (retryNum >= _policy.MaxRetryNum)
                    throw;

                var sleep = (int)Math.Pow(retryNum + 1, 2.0) + _policy.RetrySleep;
                sleep = (_policy.MaxSleep < sleep)
                    ? _policy.MaxSleep
                    : (sleep < _policy.MinSleep) ? _policy.MinSleep : sleep;

                Thread.Sleep(sleep);
            }
        }

        if (finallyAction != null)
            finallyAction();
    }

    /// <summary>
    /// リトライをかけながら指定された処理を実行します。
    /// </summary>
    /// <param name="executeAction">実行する処理</param>
    /// <param name="finallyAction">全てのリトライが完了した後に行う処理</param>
    /// <returns>処理実行結果</returns>
    public virtual T Execute<T>(Func<T> executeAction, Func<T> finallyAction = null)
    {
        for (var retryNum = 0; retryNum < _policy.MaxRetryNum; retryNum++)
        {
            try
            {
                return executeAction();
            }
            catch (Exception ex)
            {
                if (retryNum >= _policy.MaxRetryNum)
                    throw;

                var sleep = (int)Math.Pow(retryNum + 1, 2.0) + _policy.RetrySleep;
                sleep = (_policy.MaxSleep < sleep)
                    ? _policy.MaxSleep
                    : (sleep < _policy.MinSleep) ? _policy.MinSleep : sleep;

                Thread.Sleep(sleep);
            }
        }

        return (finallyAction != null) ? finallyAction() : default(T);
    }

    /// <summary>
    /// リトライをかけながら指定された処理を実行します。
    /// </summary>
    /// <param name="executeAction">実行する処理</param>
    /// <param name="errorAction">処理実行時に発生したエラー処理</param>
    /// <param name="finallyAction">全てのリトライが完了した後に行う処理</param>
    /// <returns>処理実行結果</returns>
    public virtual T Execute<T>(Func<T> executeAction, Action<Exception> errorAction, Func<T> finallyAction = null)
    {
        for (var retryNum = 0; retryNum < _policy.MaxRetryNum; retryNum++)
        {
            try
            {
                return executeAction();
            }
            catch (Exception ex)
            {
                errorAction(ex);

                if (retryNum >= _policy.MaxRetryNum)
                    throw;

                var sleep = (int)Math.Pow(retryNum + 1, 2.0) + _policy.RetrySleep;
                sleep = (_policy.MaxSleep < sleep)
                    ? _policy.MaxSleep
                    : (sleep < _policy.MinSleep) ? _policy.MinSleep : sleep;

                Thread.Sleep(sleep);
            }
        }
                    
        return (finallyAction != null) ? finallyAction() : default(T);
    }
}
```

指定された最大リトライ回数までリトライを重ねるごとにスリープの時間を徐々に長くしています。

リトライの方法を変えたいのであれば```virtual```メソッドを```override```することで可能になります。

使い方はこんなカンジ。
(例はADO.NETでAzure SQL Databaseに接続する時にリトライをかける。)

```csharp
public static class SqlConnectionExtensions
{
    // リトライをかけるSqlExceptioのNumber
    private static readonly HashSet<int> TransientErrorNumbers = new HashSet<int>
    {
        50000,
        // The service has encountered an error processing your request. Please try again.
        40197,
        // The service is currently busy. Retry the request after 10 seconds.
        40501,
        // A transport-level error has occurred when receiving results from the server. An established connection was aborted by the software in your host machine.
        10053,
        // A transport-level error has occurred when sending the request to the server. (provider: TCP Provider, error: 0 – An existing connection was forcibly closed by the remote host.)
        10054,
        // A network-related or instance-specific error occurred while establishing a connection to SQL Server.
        // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections.
        // (provider: TCP Provider, error: 0 – A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.)
        10060,
        // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer support, and provide them the session tracing ID of ZZZZZ.
        40613,
        // The service has encountered an error processing your request. Please try again.
        40143,
        // The client was unable to establish a connection because of an error during connection initialization process before login.
        // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. (provider: TCP Provider, error: 0 – An existing connection was forcibly closed by the remote host.)
        233,
        // A transport-level error has occurred when receiving results from the server. (provider: TCP Provider, error: 0 - The semaphore timeout period has expired.)
        121,
        // A connection was successfully established with the server, but then an error occurred during the login process. (provider: TCP Provider, error: 0 – The specified network name is no longer available.)
        64,
        // An error has occurred while establishing a connection to the server.
        // When connecting to SQL Server, this failure may be caused by the fact that under the default settings SQL Server does not allow remote connections.
        // (provider: Named Pipes Provider, error: 40 - Could not open a connection to SQL Server ) (.Net SqlClient Data Provider).
        53,
        // The instance of SQL Server you attempted to connect to does not support encryption.
        20,
        // Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. (Microsoft SQL Server, Error: -2).
        -2
    };

    /// <summary>
    /// リトライをかけながら接続を開始します。
    /// </summary>
    public static void OpenWithRetry(this SqlConnection connection)
    {
        var retry = new RetryExecutor(new ConnectionOpenRetryPolicy());
        retry.Execute(
        // リトライをかける処理
        connection.Open,
        // エラーが発生した時の処理
        exception =>
        {
            var sqlException = exception as SqlException;
            if (sqlException == null)
                throw exception;

            if (!TransientErrorNumbers.Contains(sqlException.Number))
                throw exception;

            // 接続に失敗したらコネクションプールをクリアする
            SqlConnection.ClearPool(connection);
        });
    }

    /// <summary>
    /// データベース接続時のリトライのルールを定義したクラスです。
    /// </summary>
    private class ConnectionOpenRetryPolicy : RetryPolicyBase
    {
        /// <summary>
        /// 最大リトライ回数
        /// </summary>
        public override int MaxRetryNum { get { return 4; } }

        /// <summary>
        /// リトライ時のベースとなる処理停止時間
        /// </summary>
        public override int RetrySleep { get { return 100; } }

        /// <summary>
        /// リトライ時の最大処理停止時間
        /// </summary>
        public override int MaxSleep { get { return 500; } }

        /// <summary>
        /// リトライ時の最小処理停止時間
        /// </summary>
        public override int MinSleep { get { return 10; } }
    }
}
```

DB/KVS/APIなど外部ストレージアクセス時に使えるんじゃないでしょうか。