Cleanest way to write retry logic?

前端 未结 29 2568
旧巷少年郎
旧巷少年郎 2020-11-22 03:01

Occasionally I have a need to retry an operation several times before giving up. My code is like:

int retries = 3;
while(true) {
  try {
    DoSomething();
         


        
相关标签:
29条回答
  • 2020-11-22 03:41

    This method allows retries on certain exception types (throws others immediately).

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

    Example usage:

    DoRetry(typeof(IOException), () => {
        using (var fs = new FileStream(requestedFilePath, FileMode.Create, FileAccess.Write))
        {
            fs.Write(entryBytes, 0, entryBytes.Length);
        }
    });
    
    0 讨论(0)
  • 2020-11-22 03:42

    I would add the following code to the accepted answer

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

    Basically the above code is making the Retry class generic so you can pass the type of the exception you want to catch for retry.

    Now use it almost in the same way but specifying the exception type

    Retry<EndpointNotFoundException>.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));
    
    0 讨论(0)
  • 2020-11-22 03:42

    I've written a small class based on answers posted here. Hopefully it will help someone: 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
    
    }
    
    0 讨论(0)
  • 2020-11-22 03:43

    You might also consider adding the exception type you want to retry for. For instance is this a timeout exception you want to retry? A database exception?

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

    You might also note that all of the other examples have a similar issue with testing for retries == 0 and either retry infinity or fail to raise exceptions when given a negative value. Also Sleep(-1000) will fail in the catch blocks above. Depends on how 'silly' you expect people to be but defensive programming never hurts.

    0 讨论(0)
  • 2020-11-22 03:44

    I had the need to pass some parameter to my method to retry, and have a result value; so i need an expression.. I build up this class that does the work (it is inspired to the the LBushkin's one) you can use it like this:

    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. the LBushkin's solution does one more retry =D

    0 讨论(0)
提交回复
热议问题