Maneira mais limpa de escrever lógica de repetição?


455

Ocasionalmente, preciso repetir uma operação várias vezes antes de desistir. Meu código é como:

int retries = 3;
while(true) {
  try {
    DoSomething();
    break; // success!
  } catch {
    if(--retries == 0) throw;
    else Thread.Sleep(1000);
  }
}

Gostaria de reescrever isso em uma função de repetição geral como:

TryThreeTimes(DoSomething);

É possível em c #? Qual seria o código para o TryThreeTimes()método?


1
Um ciclo simples não é suficiente? Por que simplesmente não iterar e executar a lógica várias vezes?
Restuta 13/10/2009

13
Pessoalmente, eu seria extremamente cauteloso com qualquer método auxiliar. Certamente é possível implementar usando lambdas, mas o padrão em si é extremamente fedorento, portanto, a introdução de um auxiliar para ele (o que implica que ele é frequentemente repetido) é, por si só, altamente suspeita e sugere fortemente um projeto geral ruim.
Pavel Minaev 13/10/09

12
No meu caso, meus DoSomething () estão fazendo coisas em máquinas remotas, como excluir arquivos ou tentar acessar uma porta de rede. Nos dois casos, existem grandes problemas de tempo para quando o DoSomething terá êxito e, devido ao afastamento, não há nenhum evento em que eu possa ouvir. Então sim, é fedorento. Sugestões são bem-vindas.
Noctonura 13/10/09

13
@PavelMinaev Por que o uso de novas tentativas sugere um projeto geral ruim? Se você escrever muito código que conecta pontos de integração, o uso de novas tentativas é definitivamente um padrão que você deve considerar seriamente.
bytedev

Respostas:


569

Instruções de captura geral que simplesmente repetem a mesma chamada podem ser perigosas se usadas como um mecanismo geral de manipulação de exceções. Dito isso, aqui está um empacotador de novas tentativas baseado em lambda que você pode usar com qualquer método. Optei por fatorar o número de tentativas e o tempo limite da nova tentativa como parâmetros para um pouco mais de flexibilidade:

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

Agora você pode usar este método utilitário para executar a nova lógica:

Retry.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));

ou:

Retry.Do(SomeFunctionThatCanFail, TimeSpan.FromSeconds(1));

ou:

int result = Retry.Do(SomeFunctionWhichReturnsInt, TimeSpan.FromSeconds(1), 4);

Ou você pode até asyncsobrecarregar.


7
+1, especialmente para aviso e verificação de erros. Eu ficaria mais confortável se isso passasse no tipo da exceção para capturar como um parâmetro genérico (onde T: Exception), no entanto.
TrueWill 13/10/2009

1
Era minha intenção que "tentativas" realmente significassem tentativas. Mas não é muito difícil mudá-lo para significar "tentativas". Contanto que o nome seja mantido significativo. Existem outras oportunidades para melhorar o código, como verificar novas tentativas negativas ou tempos limites negativos - por exemplo. Eu os omiti principalmente para manter o exemplo simples ... mas, novamente, na prática, esses provavelmente seriam bons aprimoramentos para a implementação.
LBushkin 14/10/2009

40
Usamos um padrão semelhante para nosso acesso ao banco de dados em um aplicativo Biztalk de alto volume, mas com duas melhorias: Temos listas negras de exceções que não devem ser repetidas e armazenamos a primeira exceção que ocorre e a lançamos quando a nova tentativa finalmente falha. A razão é que a segunda e as seguintes exceções geralmente são diferentes da primeira. Nesse caso, você oculta o problema inicial ao refazer novamente a última exceção.
TToni 22/03/12

2
@ Dexters Lançamos uma nova exceção com a exceção original como exceção interna. O rastreamento da pilha original está disponível como atributo nas exceções internas.
TToni

7
Você também pode tentar usar uma biblioteca de código aberto, como Polly, para lidar com isso. Há muito mais flexibilidade para aguardar entre tentativas e foi validado por muitos outros que usaram o projeto. Exemplo: Policy.Handle<DivideByZeroException>().WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });
Todd Meinershagen

222

Você deveria tentar Polly . É uma biblioteca .NET criada por mim que permite que os desenvolvedores expressem políticas transitórias de manipulação de exceções, como Repetir, Repetir para sempre, Aguardar e Repetir ou Disjuntor de maneira fluente.

Exemplo

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

1
O que é realmente o delegado OnRetry? Suponho que seja o que precisamos executar quando ocorrer uma exceção. Portanto, quando ocorrer uma exceção, o delegado do OnRetry chamará e depois executará o delegado. É assim?
User6395764

61

Esta é possivelmente uma má ideia. Primeiro, é emblemático da máxima "a definição de insanidade está fazendo a mesma coisa duas vezes e esperando resultados diferentes a cada vez". Segundo, esse padrão de codificação não se compõe bem consigo mesmo. Por exemplo:

Suponha que sua camada de hardware de rede reenvie um pacote três vezes em caso de falha, esperando, digamos, um segundo entre falhas.

Agora, suponha que a camada de software reenvie uma notificação sobre uma falha três vezes na falha do pacote.

Agora, suponha que a camada de notificação reative a notificação três vezes em uma falha na entrega da notificação.

Agora, suponha que a camada de relatório de erros reative a camada de notificação três vezes em uma falha de notificação.

E agora suponha que o servidor da Web reative o relatório de erros três vezes na falha do erro.

E agora suponha que o cliente da Web reenvie a solicitação três vezes após receber um erro do servidor.

Agora, suponha que a linha no comutador de rede que deveria rotear a notificação para o administrador esteja desconectada. Quando o usuário do cliente da Web finalmente recebe sua mensagem de erro? Eu chego cerca de doze minutos depois.

Para que você não pense que este é apenas um exemplo bobo: vimos esse bug no código do cliente, embora muito, muito pior do que descrevi aqui. No código do cliente em particular, a diferença entre a condição de erro ocorrida e a finalmente relatada ao usuário era de várias semanas, porque muitas camadas estavam tentando novamente automaticamente com esperas. Imagine o que aconteceria se houvesse dez tentativas em vez de três .

Geralmente, a coisa certa a fazer com uma condição de erro é denunciá-la imediatamente e deixar que o usuário decida o que fazer. Se o usuário desejar criar uma política de novas tentativas automáticas, crie-a no nível apropriado na abstração do software.


19
+1. Raymond compartilha um exemplo da vida real aqui, blogs.msdn.com/oldnewthing/archive/2005/11/07/489807.aspx
SolutionYogi

210
-1 Este conselho é inútil para falhas transitórias da rede encontradas por sistemas automatizados de processamento em lote.
nohat 24/09/10

15
Não tenho certeza se isso está dizendo "Não faça", seguido de "faça". A maioria das pessoas que fazem essa pergunta provavelmente são as que trabalham na abstração do software.
21712 Jim

44
Quando você tem tarefas em lote de longa execução que usam recursos de rede, como serviços da Web, não pode esperar que a rede seja 100% confiável. Haverá tempos de espera ocasionais, desconexões de soquete, possivelmente até falhas espúrias de roteamento ou interrupções no servidor que ocorrerem enquanto você o estiver usando. Uma opção é falhar, mas isso pode significar reiniciar um trabalho demorado mais tarde. Outra opção é tentar novamente algumas vezes com atraso adequado para verificar se é um problema temporário e falhar. Concordo com a composição, da qual você deve estar ciente ... mas às vezes é a melhor escolha.
Erik Funkenbusch

18
Eu acho que a citação que você usou no início da sua resposta é interessante. "Esperar resultados diferentes" é apenas insanidade se a experiência anterior fornecer regularmente os mesmos resultados. Embora o software seja construído com a promessa de consistência, há definitivamente circunstâncias em que somos obrigados a interagir com forças não confiáveis ​​fora de nosso controle.
Michael Richardson

49
public void TryThreeTimes(Action action)
{
    var tries = 3;
    while (true) {
        try {
            action();
            break; // success!
        } catch {
            if (--tries == 0)
                throw;
            Thread.Sleep(1000);
        }
    }
}

Então você ligaria para:

TryThreeTimes(DoSomething);

...ou alternativamente...

TryThreeTimes(() => DoSomethingElse(withLocalVariable));

Uma opção mais flexível:

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

Para ser usado como:

DoWithRetry(DoSomething, TimeSpan.FromSeconds(2), tryCount: 10);

Uma versão mais moderna, com suporte para assíncrono / espera:

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

Para ser usado como:

await DoWithRetryAsync(DoSomethingAsync, TimeSpan.FromSeconds(2), tryCount: 10);

2
de preferência, mude o if para: --retryCount <= 0porque isso continuará para sempre, se você desejar desativar as novas tentativas, definindo-o como 0. Tecnicamente, o termo retryCountnão é um nome muito bom, porque não será repetido se você o definir como 1. renomear para tryCountcolocar o - para trás.
Stefanvds

2
@ saille Eu concordo. No entanto, o OP (e todas as outras respostas) estão usando Thread.Sleep. As alternativas são usar temporizadores, ou mais provavelmente hoje em dia usar asyncpara tentar novamente, com Task.Delay.
Drew Noakes #

2
Eu adicionei uma versão assíncrona.
de Drew Noakes

quebre se ação returns true? Func<bool>
Kanjet

32

O bloco de aplicativos Transient Fault Handling fornece uma coleção extensível de estratégias de repetição, incluindo:

  • Incremental
  • Intervalo fixo
  • Retorno exponencial

Ele também inclui uma coleção de estratégias de detecção de erros para serviços baseados em nuvem.

Para mais informações, consulte este capítulo do Guia do desenvolvedor.

Disponível via NuGet (procure por ' topázio ').


1
Interessante. Você pode usar isso fora do Windows Azure, digamos em um aplicativo Winforms?
Matthew Bloqueio

6
Absolutamente. Use o mecanismo de nova tentativa e forneça suas próprias estratégias de detecção. Nós intencionalmente os dissociamos. Encontre o pacote principal de nuget aqui: nuget.org/packages/TransientFaultHandling.Core
Grigori Melnik

2
Além disso, o projeto agora está sob o Apache 2.0 e está aceitando contribuições da comunidade. aka.ms/entlibopen
Grigori Melnik

1
@Alex. Os pedaços dele estão chegando à plataforma.
Grigori Melnik

2
Agora isso está obsoleto e, pela última vez que o usei, continha alguns bugs que, até onde eu sei, não estavam e nunca serão corrigidos: github.com/MicrosoftArchive/… .
Ohad Schneider

15

Permitindo funções e repetir mensagens

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

RetryMethod para retval é True, oumax retries?
Kiquenet 27/09/18

14

Você também pode considerar adicionar o tipo de exceção para o qual deseja tentar novamente. Por exemplo, essa é uma exceção de tempo limite que você deseja tentar novamente? Uma exceção de banco de dados?

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

Você também pode observar que todos os outros exemplos têm um problema semelhante ao testar tentativas == 0 e tentar novamente o infinito ou falhar ao gerar exceções quando receber um valor negativo. Também a suspensão (-1000) falhará nos blocos de captura acima. Depende de quão 'tolo' você espera que as pessoas sejam, mas a programação defensiva nunca é demais.


9
+1, mas por que não RetryForException <T> (...) onde T: Exception e depois captura (T e)? Apenas tentei e funciona perfeitamente.
TrueWill 14/10/2009

Ou aqui, já que eu não preciso fazer nada com o Type, desde que eu imaginei que um parâmetro antigo simples seria suficiente.
Csharptest.net 14/10/09

@TrueWill aparentemente catch (T ex) tem alguns bugs de acordo com este post stackoverflow.com/questions/1577760/...
csharptest.net

3
Atualização: Na verdade, uma implementação melhor que eu estou usando requer um delegado Predicate <Exception> que retorna true se uma nova tentativa for apropriada. Isso permite que você use códigos de erro nativos ou outras propriedades da exceção para determinar se uma nova tentativa é aplicável. Por exemplo, códigos HTTP 503.
Csharptest.net

1
"O modo de suspensão (-1000) também falhará nos blocos de captura acima" ... use um TimeSpan e você não obterá esse problema. Além disso, o TimeSpan é muito mais flexível e auto-descritivo. A partir da sua assinatura "int retryTimeout", como saber se retryTimeout é MS, segundos, minutos, anos? ;-)
bytedev 24/08

13

Sou fã de métodos de recursão e extensão, então aqui estão meus dois centavos:

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

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

7

Com base no trabalho anterior, pensei em aprimorar a lógica de repetição de três maneiras:

  1. Especificando que tipo de exceção capturar / tentar novamente. Este é o aprimoramento primário, pois tentar novamente para qualquer exceção é simplesmente errado.
  2. Não aninhar a última tentativa em uma tentativa / captura, obtendo um desempenho um pouco melhor
  3. Tornando-o um Actionmétodo de extensão

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

O método pode ser chamado assim (métodos anônimos também podem ser usados, é claro):

new Action( AMethodThatMightThrowIntermittentException )
  .InvokeAndRetryOnException<IntermittentException>( 2, TimeSpan.FromSeconds( 1 ) );

1
Isto e excelente. Mas, pessoalmente, eu não chamaria de 'retryTimeout', pois não é realmente um tempo limite. 'RetryDelay', talvez?
Holf

7

Mantenha as coisas simples com o 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
Estou curioso, isso geraria uma quantidade insana de threads com uma alta contagem de repetições e intervalo por causa do retorno do mesmo método esperado?
HuntK24

6

Use Polly

https://github.com/App-vNext/Polly-Samples

Aqui está uma nova tentativa genérica que eu uso com a 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;
}

Use-o assim

var result = Retry(() => MyFunction()), 3);

5

Implementou a resposta do LBushkin da maneira mais recente:

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

e para usá-lo:

await Retry.Do([TaskFunction], retryInterval, retryAttempts);

enquanto a função [TaskFunction]pode ser Task<T>ou apenas Task.


1
Obrigado Fabian! Isso deve ser votado até o topo!
31718 JamesHoux

1
@ MarkLauter a resposta curta é sim. ;-)
Fabian Bigler

4

Eu implementaria isso:

public static bool Retry(int maxRetries, Func<bool, bool> method)
{
    while (maxRetries > 0)
    {
        if (method(maxRetries == 1))
        {
            return true;
        }
        maxRetries--;
    }
    return false;        
}

Eu não usaria exceções da maneira como são usadas nos outros exemplos. Parece-me que, se esperamos a possibilidade de um método não ter sucesso, seu fracasso não é uma exceção. Portanto, o método que estou chamando deve retornar verdadeiro se for bem-sucedido e falso se falhar.

Por que é um Func<bool, bool>e não apenas um Func<bool>? Para que se eu quiser um método seja capaz de gerar uma exceção em caso de falha, tenho uma maneira de informar que essa é a última tentativa.

Então, eu posso usá-lo com código como:

Retry(5, delegate(bool lastIteration)
   {
       // do stuff
       if (!succeeded && lastIteration)
       {
          throw new InvalidOperationException(...)
       }
       return succeeded;
   });

ou

if (!Retry(5, delegate(bool lastIteration)
   {
       // do stuff
       return succeeded;
   }))
{
   Console.WriteLine("Well, that didn't work.");
}

Se passar um parâmetro que o método não usa for desagradável, é trivial implementar uma sobrecarga Retryque exige apenas um pouco Func<bool>.


1
+1 para evitar a exceção. Embora eu fizesse uma nova tentativa nula (...) e jogasse alguma coisa? Retornos booleanos e / ou códigos de retorno são frequentemente ignorados.
Csharptest.net 13/10/09

1
"se esperamos a possibilidade de um método não ter êxito, sua falha não é uma exceção" - embora isso seja verdade em alguns casos, a exceção não precisa implicar excepcional. É para tratamento de erros. Não há garantia de que o chamador verifique um resultado booleano. Não é uma garantia de que uma exceção será tratada (pelo tempo de execução de encerrar o aplicativo se nada mais faz).
TrueWill 14/10/2009

Não consigo encontrar a referência, mas acredito que o .NET define uma exceção como "um método não fez o que disse que faria". 1 objetivo é usar exceções para indicar um problema, em vez do padrão Win32, de exigir que o chamador verifique o valor de retorno se a função tiver êxito ou não.
Noctonura 14/10/2009

Mas as exceções não meramente "indicam um problema". Eles também incluem uma grande quantidade de informações de diagnóstico que custam tempo e memória para serem compiladas. Existem claramente situações em que isso não importa nem um pouco. Mas há muito onde isso acontece. O .NET não usa exceções para o fluxo de controle (compare, digamos, com o uso da StopIterationexceção pelo Python), e há um motivo.
1111 Robert Rossney

O TryDopadrão do método é uma inclinação escorregadia. Antes que você perceba, toda a sua pilha de chamadas consistirá em TryDométodos. Foram criadas exceções para evitar essa confusão.
HappyNomad


2

Para aqueles que desejam ter a opção de tentar novamente qualquer exceção ou definir explicitamente o tipo de exceção, use o seguinte:

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

Eu precisava de um método que ofereça suporte ao cancelamento. Enquanto estava nisso, adicionei suporte para retornar falhas intermediárias.

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

Você pode usar a Retryfunção assim, tente novamente 3 vezes com um atraso de 10 segundos, mas sem cancelamento.

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

Ou tente eternamente a cada cinco segundos, a menos que cancelado.

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

Como você pode imaginar, no meu código-fonte sobrecarreguei a Retryfunção para suportar os diferentes tipos de delgate que desejo usar.


2

Este método permite tentativas em certos tipos de exceção (lança outros imediatamente).

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

Exemplo de uso:

DoRetry(typeof(IOException), () => {
    using (var fs = new FileStream(requestedFilePath, FileMode.Create, FileAccess.Write))
    {
        fs.Write(entryBytes, 0, entryBytes.Length);
    }
});

1

Atualização após 6 anos: agora considero que a abordagem abaixo é muito ruim. Para criar uma lógica de nova tentativa, devemos considerar o uso de uma biblioteca como Polly.


Minha asyncimplementação do método de repetição:

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

Pontos-chave: eu usei .ConfigureAwait(false);e em Func<dynamic>vez dissoFunc<T>


Isso não fornece uma resposta para a pergunta. Considere postar sua resposta como uma nova pergunta, usando o botão "Fazer pergunta" na parte superior da página e, em seguida, publique sua própria resposta para compartilhar o que aprendeu com a comunidade.
Elixenide

Muito mais simples com o C # 5.0 do que codereview.stackexchange.com/q/55983/54000, mas talvez o CansellactionToken deva ser injetado.
SerG

Há um problema com esta implementação. Após a nova tentativa final, logo antes de desistir, Task.Delayé chamado sem motivo.
HappyNomad

@ HappyNomad, esta é uma resposta de 6 anos e agora considero que é uma péssima abordagem criar uma lógica de repetição :)) obrigado pela notificação. Vou atualizar minha resposta de acordo com essa consideração.
Cihan Uygun

0

Ou que tal fazê-lo um pouco mais limpo ....

int retries = 3;
while (retries > 0)
{
  if (DoSomething())
  {
    retries = 0;
  }
  else
  {
    retries--;
  }
}

Acredito que lançar exceções geralmente deve ser evitado como um mecanismo, a menos que você as passe entre limites (como criar uma biblioteca que outras pessoas possam usar). Por que não apenas DoSomething()retornar o comando truese foi bem-sucedido ou falsenão?

EDIT: E isso pode ser encapsulado dentro de uma função como outros sugeriram também. O único problema é se você não estiver escrevendo a DoSomething()função


7
"Acredito que lançar exceções geralmente deve ser evitado como um mecanismo, a menos que você as passe entre fronteiras" - discordo completamente. Como você sabe que o chamador verificou seu retorno falso (ou pior, nulo)? POR QUE o código falhou? False não diz mais nada. E se o chamador tiver que passar a falha para cima da pilha? Leia msdn.microsoft.com/en-us/library/ms229014.aspx - estes são para bibliotecas, mas fazem tanto sentido para o código interno. E em uma equipe, outras pessoas provavelmente chamarão seu código.
TrueWill 14/10/2009

0

Eu tive a necessidade de passar algum parâmetro para o meu método para tentar novamente e ter um valor de resultado; então eu preciso de uma expressão .. Eu construo essa classe que faz o trabalho (ela é inspirada na do LBushkin), você pode usá-la assim:

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. a solução da LBushkin faz mais uma tentativa = D


0

Eu adicionaria o seguinte código à resposta aceita

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

Basicamente, o código acima está fazendo o Retry classe genérica, para que você possa passar o tipo da exceção que deseja capturar para tentar novamente.

Agora use-o quase da mesma maneira, mas especificando o tipo de exceção

Retry<EndpointNotFoundException>.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));

O loop for sempre será executado algumas vezes (com base em seu retryCount), mesmo que o código no loop TRY CATCH tenha sido executado sem exceções. Eu sugeriria definir retryCount igual à repetição var no loop try, para que o loop for pare de passar por ele.
scre_www

@scre_www Eu acredito que você está enganado. Se actionnão lançar, então Doretornará assim breakpara longe do forloop.
HappyNomad

De qualquer forma, há um problema com esta implementação. Após a nova tentativa final, logo antes de desistir, Thread.Sleepé chamado sem motivo.
HappyNomad

0

Eu sei que essa resposta é muito antiga, mas eu só queria comentar sobre isso, porque encontrei problemas usando essas instruções enquanto, faça, qualquer que seja a declaração dos contadores.

Ao longo dos anos, decidi por uma abordagem melhor, eu acho. Isso é usar algum tipo de agregação de evento, como extensões reativas "Assunto" ou algo parecido. Quando uma tentativa falha, você simplesmente publica um evento dizendo que a tentativa falhou e a função agregadora reprograma o evento. Isso permite que você tenha muito mais controle sobre a nova tentativa sem poluir a chamada em si com vários loops de repetição e o que não. Nem você está amarrando um único fio com vários fios dormentes.


0

Simplifique em C #, Java ou outras linguagens:

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

e use no seu código muito simples:

 void simpleMethod(){
    //some code

    if(ShouldRetryHandler.shouldRetry()){
    //do some repetitive work
     }

    //some code    
    }

ou você pode usá-lo em métodos recursivos:

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

Com o que você faz Request.BadRequest?
Danh

0

Auxiliar de nova tentativa: uma implementação java genérica que contém tentativas retornáveis ​​e do tipo nulo.

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

Uso:

    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

Aqui está uma versão async/ awaitque agrega exceções e suporta o cancelamento.

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

Como throw;é a única maneira de terminar o loop infinito, esse método está implementando "tente até que falhe N vezes" e não o desejado "tente até Nvezes até que seja bem-sucedido". Você precisa de uma break;ou return;depois da chamada para ThingToTryDelegate();que, caso contrário, ela seja chamada continuamente se nunca falhar. Além disso, isso não será compilado porque o primeiro parâmetro de TryNTimesnão tem nome. -1.
BACON

-1

Eu escrevi uma turma pequena com base nas respostas postadas aqui. Espero que ajude alguém: 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

Eu implementei uma versão assíncrona da resposta aceita assim - e parece funcionar muito bem - algum comentário?


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

E, chame-o simplesmente assim:

var result = await Retry.DoAsync(() => MyAsyncMethod(), TimeSpan.FromSeconds(5), 4);

Thread.Sleep? O bloqueio de um encadeamento nega os benefícios da assincronia. Também tenho certeza de que a Task DoAsync()versão deve aceitar um argumento do tipo Func<Task>.
Theodor Zoulias
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.