¿Forma más limpia de escribir lógica de reintento?

455

Ocasionalmente, necesito volver a intentar una operación varias veces antes de rendirme. Mi código es como:

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

Me gustaría volver a escribir esto en una función de reintento general como:

TryThreeTimes(DoSomething);

¿Es posible en C #? ¿Cuál sería el código para el TryThreeTimes()método?

noctonura
fuente
1
¿Un ciclo simple no es suficiente? ¿Por qué simplemente no repetir y ejecutar la lógica varias veces?
Restuta
13
Personalmente, sería extremadamente cauteloso con cualquier método de ayuda. Ciertamente es posible implementar el uso de lambdas, pero el patrón en sí es extremadamente maloliente, por lo que introducir un ayudante para él (lo que implica que se repite con frecuencia) es en sí mismo altamente sospechoso y sugiere fuertemente un mal diseño general.
Pavel Minaev
12
En mi caso, mis DoSomething () s están haciendo cosas en máquinas remotas, como eliminar archivos o intentar alcanzar un puerto de red. En ambos casos, existen problemas importantes de tiempo para que DoSomething tenga éxito y, debido a la lejanía, no hay ningún evento que pueda escuchar. Así que sí, es maloliente. Sugerencias bienvenidas.
noctonura
13
@PavelMinaev ¿por qué usar reintentos sugiere un mal diseño general? Si escribe una gran cantidad de código que conecta puntos de integración, el uso de reintentos es definitivamente un patrón que debería considerar seriamente usar.
bytedev

Respuestas:

569

Las declaraciones generales de captura que simplemente reintentan la misma llamada pueden ser peligrosas si se usan como un mecanismo general de manejo de excepciones. Dicho esto, aquí hay un contenedor de reintentos basado en lambda que puede usar con cualquier método. Elegí factorizar el número de reintentos y el tiempo de espera de reintentos como parámetros para un poco más de flexibilidad:

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

Ahora puede usar este método de utilidad para realizar la lógica de reintento:

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

o:

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

o:

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

O incluso podrías hacer una asyncsobrecarga.

LBushkin
fuente
77
+1, especialmente para la advertencia y la comprobación de errores. Sin embargo, me sentiría más cómodo si esto pasara el tipo de excepción para atraparlo como un parámetro genérico (donde T: excepción).
TrueWill
1
Mi intención era que "reintentos" en realidad significara reintentos. Pero no es demasiado difícil cambiarlo para que signifique "intentos". Mientras el nombre se mantenga significativo. Existen otras oportunidades para mejorar el código, como la comprobación de reintentos negativos o tiempos de espera negativos, por ejemplo. Los omití principalmente para mantener el ejemplo simple ... pero, de nuevo, en la práctica probablemente serían buenas mejoras para la implementación.
LBushkin el
40
Utilizamos un patrón similar para nuestro acceso a la base de datos en una aplicación Biztalk de alto volumen, pero con dos mejoras: tenemos listas negras para las excepciones que no se deben volver a intentar y almacenamos la primera excepción que ocurre y la lanzamos cuando el reintento finalmente falla. La razón es que la segunda y siguientes excepciones a menudo son diferentes de la primera. En ese caso, oculta el problema inicial cuando vuelve a lanzar solo la última excepción.
TToni
2
@Dexters Lanzamos una nueva excepción con la excepción original como excepción interna. El seguimiento de pila original está disponible como atributo de las excepciones internas.
TToni
77
También puede intentar usar una biblioteca de código abierto como Polly para manejar esto. Hay mucha más flexibilidad para esperar entre reintentos y ha sido validado por muchos otros que han utilizado el proyecto. Ejemplo: Policy.Handle<DivideByZeroException>().WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });
Todd Meinershagen
222

Deberías probar Polly . Es una biblioteca .NET escrita por mí que permite a los desarrolladores expresar políticas transitorias de manejo de excepciones como Retry, Retry Forever, Wait and Retry o Circuit Breaker de manera fluida.

Ejemplo

Policy
    .Handle<SqlException>(ex => ex.Number == 1205)
    .Or<ArgumentException>(ex => ex.ParamName == "example")
    .WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(3))
    .Execute(() => DoSomething());
Michael Wolfenden
fuente
1
¿Qué es en realidad el delegado de OnRetry? Supongo que es lo que debemos realizar cuando se produce una excepción. Entonces, cuando se produce una excepción, el delegado de OnRetry llamará y luego ejecutará el delegado. ¿Es tan?
user6395764
61

Posiblemente sea una mala idea. Primero, es emblemático de la máxima "la definición de locura es hacer lo mismo dos veces y esperar resultados diferentes cada vez". En segundo lugar, este patrón de codificación no se compone bien consigo mismo. Por ejemplo:

Suponga que su capa de hardware de red reenvía un paquete tres veces en caso de falla, esperando, por ejemplo, un segundo entre fallas.

Ahora suponga que la capa de software reenvía una notificación sobre una falla tres veces en la falla del paquete.

Ahora suponga que la capa de notificación reactiva la notificación tres veces en un error de entrega de notificación.

Ahora suponga que la capa de informe de errores reactiva la capa de notificación tres veces en un error de notificación.

Y ahora suponga que el servidor web reactiva el error informando tres veces en caso de error.

Y ahora suponga que el cliente web reenvía la solicitud tres veces al recibir un error del servidor.

Ahora suponga que la línea en el conmutador de red que debe enrutar la notificación al administrador está desconectada. ¿Cuándo finalmente el usuario del cliente web recibe su mensaje de error? Lo hago unos doce minutos después.

No sea que piense que esto es solo un ejemplo tonto: hemos visto este error en el código del cliente, aunque mucho, mucho peor de lo que he descrito aquí. En el código de cliente en particular, la brecha entre la condición de error que sucedió y finalmente se informó al usuario fue de varias semanas porque muchas capas estaban volviendo a intentarlo automáticamente con esperas. Solo imagine lo que sucedería si hubiera diez reintentos en lugar de tres .

Por lo general, lo correcto con una condición de error es informarlo de inmediato y dejar que el usuario decida qué hacer. Si el usuario desea crear una política de reintentos automáticos, permítale crear esa política en el nivel apropiado en la abstracción del software.

Eric Lippert
fuente
19
+1. Raymond comparte un ejemplo de la vida real aquí, blogs.msdn.com/oldnewthing/archive/2005/11/07/489807.aspx
SolutionYogi
210
-1 Este consejo es inútil para fallas de red transitorias encontradas por sistemas automatizados de procesamiento por lotes.
nohat
15
No estoy seguro si esto dice "No lo hagas" seguido de "hacerlo". La mayoría de las personas que hacen esta pregunta son probablemente las personas que trabajan en la abstracción del software.
Jim L
44
Cuando tiene trabajos por lotes de larga ejecución que utilizan recursos de red, como servicios web, no puede esperar que la red sea 100% confiable. Habrá tiempos de espera ocasionales, desconexiones de sockets, posiblemente incluso fallas de enrutamiento espurias o interrupciones del servidor que ocurran mientras lo está usando. Una opción es fallar, pero eso puede significar reiniciar un trabajo largo más tarde. Otra opción es volver a intentarlo varias veces con la demora adecuada para ver si se trata de un problema temporal y luego fallar. Estoy de acuerdo con la composición, que debes tener en cuenta ... pero a veces es la mejor opción.
Erik Funkenbusch
18
Creo que la cita que utilizó al comienzo de su respuesta es interesante. "Esperar resultados diferentes" solo es una locura si la experiencia previa le da los mismos resultados regularmente. Si bien el software se basa en una promesa de coherencia, definitivamente hay circunstancias en las que estamos obligados a interactuar con fuerzas poco confiables fuera de nuestro control.
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);
        }
    }
}

Entonces llamarías:

TryThreeTimes(DoSomething);

...o alternativamente...

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

Una opción más flexible:

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 utilizado como:

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

Una versión más moderna con soporte para async / wait:

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 utilizado como:

await DoWithRetryAsync(DoSomethingAsync, TimeSpan.FromSeconds(2), tryCount: 10);
Drew Noakes
fuente
2
preferiblemente cambie el if a: --retryCount <= 0porque esto continuará para siempre si desea deshabilitar los reintentos configurándolo en 0. Técnicamente el término retryCountno es realmente un buen nombre, porque no volverá a intentarlo si lo configura en 1. o renombrar a tryCount, o poner el - atrás.
Stefanvds
2
@saille estoy de acuerdo. Sin embargo, el OP (y todas las demás respuestas) están utilizando Thread.Sleep. Las alternativas son usar temporizadores, o más probablemente hoy en día asyncpara volver a intentarlo, con Task.Delay.
Drew Noakes
2
He agregado una versión asíncrona.
Drew Noakes
¿Solo romper si hay acción returns true? Func<bool>
Kiquenet
32

El bloque de aplicación Transitor Fault Handling proporciona una colección extensible de estrategias de reintento que incluyen:

  • Incremental
  • Intervalo fijo
  • Retroceso exponencial

También incluye una colección de estrategias de detección de errores para servicios basados ​​en la nube.

Para obtener más información, consulte este capítulo de la Guía del desarrollador.

Disponible a través de NuGet (busque ' topaz ').

Grigori Melnik
fuente
1
Interesante. ¿Puedes usar esto fuera de Windows Azure, por ejemplo, en una aplicación Winforms?
Matthew Lock
66
Absolutamente. Use el mecanismo de reintento principal y proporcione sus propias estrategias de detección. Los desacoplamos intencionalmente. Encuentre el paquete principal de nuget aquí: nuget.org/packages/TransientFaultHandling.Core
Grigori Melnik el
2
Además, el proyecto ahora está bajo Apache 2.0 y acepta contribuciones de la comunidad. aka.ms/entlibopen
Grigori Melnik
1
@Alex. Las piezas están llegando a la plataforma.
Grigori Melnik el
2
Esto ahora está en desuso, y la última vez que lo usé contenía algunos errores que, hasta donde yo sé, no estaban, y nunca se solucionarán: github.com/MicrosoftArchive/… .
Ohad Schneider
15

Permitir funciones y reintentar mensajes

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;
}
Brian
fuente
RetryMethod to retval are True, ormax retries?
Kiquenet
14

También puede considerar agregar el tipo de excepción para el que desea volver a intentar. Por ejemplo, ¿es esta una excepción de tiempo de espera que desea volver a intentar? ¿Una excepción de base de datos?

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

También puede observar que todos los otros ejemplos tienen un problema similar con la prueba de reintentos == 0 y reintentan infinito o no generan excepciones cuando se les da un valor negativo. También Sleep (-1000) fallará en los bloques catch anteriores. Depende de cuán 'tonto' esperes que sea la gente, pero la programación defensiva nunca está de más.

csharptest.net
fuente
99
+1, pero ¿por qué no hacer RetryForException <T> (...) donde T: Excepción, luego capturar (T e)? Solo lo probé y funciona perfectamente.
TrueWill
Ya sea aquí o no, ya que no necesito hacer nada con el Tipo provisto, pensé que un parámetro viejo y simple funcionaría.
csharptest.net
@TrueWill aparentemente catch (T ex) tiene algunos errores de acuerdo con esta publicación stackoverflow.com/questions/1577760/…
csharptest.net
3
Actualización: en realidad, una mejor implementación que he estado usando toma un delegado Predicate <Exception> que devuelve verdadero si un reintento es apropiado. Esto le permite utilizar códigos de error nativos u otras propiedades de la excepción para determinar si un reintento es aplicable. Por ejemplo, los códigos HTTP 503.
csharptest.net
1
"También Sleep (-1000) fallará en los bloques catch anteriores" ... use un TimeSpan y no obtendrá este problema. Además, TimeSpan es mucho más flexible y descriptivo. A partir de su firma de "int retryTimeout", ¿cómo sé si retryTimeout es MS, segundos, minutos, años? ;-)
bytedev
13

Soy fanático de los métodos de recursión y extensión, así que aquí están mis dos centavos:

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

        InvokeWithRetries(@this, --numberOfRetries);
    }
}
Martin RL
fuente
7

Sobre la base del trabajo anterior, pensé en mejorar la lógica de reintento de tres maneras:

  1. Especificando qué tipo de excepción capturar / reintentar. Esta es la mejora principal, ya que volver a intentar cualquier excepción es simplemente incorrecto.
  2. No anidar el último intento en un intento / captura, logrando un rendimiento ligeramente mejor
  3. Haciéndolo un Actionmétodo de extensión

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

El método se puede invocar así (también se pueden usar métodos anónimos, por supuesto):

new Action( AMethodThatMightThrowIntermittentException )
  .InvokeAndRetryOnException<IntermittentException>( 2, TimeSpan.FromSeconds( 1 ) );
Igor Pashchuk
fuente
1
Esto es excelente. Pero personalmente no lo llamaría 'retryTimeout' ya que no es realmente un Timeout. ¿'RetryDelay', tal vez?
Holf
7

Mantenlo simple con 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);
    }
}
Anders Skovborg
fuente
2
Tengo curiosidad, ¿generaría esto una cantidad increíble de hilos con un recuento e intervalo de reintentos altos debido a la devolución del mismo método esperado?
HuntK24
6

Use Polly

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

Aquí hay un reintento genérico que uso con 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;
}

Úselo así

var result = Retry(() => MyFunction()), 3);
Erik Bergstedt
fuente
5

Implementé la respuesta de LBushkin de la última manera:

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

y para usarlo:

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

mientras que la función [TaskFunction]puede ser Task<T>o simplemente Task.

Fabian Bigler
fuente
1
¡Gracias Fabián! ¡Esto debería ser votado hasta la cima!
JamesHoux
1
@ MarkLauter la respuesta corta es sí. ;-)
Fabian Bigler
4

Implementaría esto:

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

No usaría excepciones como se usan en los otros ejemplos. Me parece que si esperamos la posibilidad de que un método no tenga éxito, su falla no es una excepción. Entonces, el método al que llamo debería devolver verdadero si tuvo éxito, y falso si falló.

¿Por qué es un Func<bool, bool>y no solo un Func<bool>? De modo que si quiero un método para poder lanzar una excepción en caso de falla, tengo una manera de informarle que este es el último intento.

Entonces podría usarlo con código como:

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

o

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

Si pasa un parámetro que el método no uso resulta ser incómodo, es trivial para aplicar una sobrecarga de Retryque sólo se necesita una Func<bool>también.

Robert Rossney
fuente
1
+1 para evitar la excepción. ¿Aunque haría un Reintento nulo (...) y arrojaría algo? Los retornos booleanos y / o los códigos de retorno se pasan por alto con demasiada frecuencia.
csharptest.net
1
"Si esperamos la posibilidad de que un método no tenga éxito, su fracaso no es una excepción", aunque eso es cierto en algunos casos, la excepción no necesariamente implica una excepción. Es para el manejo de errores. No hay garantía de que la persona que llama verificará un resultado booleano. No es una garantía de que una excepción será manejado (por el tiempo de ejecución de cierre de la aplicación si nada más lo hace).
TrueWill
No puedo encontrar la referencia, pero creo que .NET define una excepción como "un método no hizo lo que dijo que haría". 1 propósito es usar excepciones para indicar un problema en lugar del patrón Win32 de requerir que la persona que llama verifique el valor de retorno si la función tuvo éxito o no.
noctonura
Pero las excepciones no solo "indican un problema". También incluyen una gran cantidad de información de diagnóstico que cuesta tiempo y memoria para compilar. Claramente, hay situaciones en las que eso no importa lo más mínimo. Pero hay muchos donde sí. .NET no usa excepciones para el flujo de control (compare, digamos, con el uso de Python de la StopIterationexcepción), y hay una razón.
Robert Rossney
El TryDopatrón del método es una pendiente resbaladiza. Antes de que te des cuenta, toda tu pila de llamadas consistirá en TryDométodos. Se inventaron excepciones para evitar tal desorden.
HappyNomad
2

El retroceso exponencial es una buena estrategia de reintento que simplemente intentar x número de veces. Puede usar una biblioteca como Polly para implementarlo.

utsavized
fuente
2

Para aquellos que desean tener la opción de volver a intentar cualquier excepción o establecer explícitamente el tipo de excepción, use esto:

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);
    }
}
mrogunlana
fuente
2

Necesitaba un método que admita la cancelación, mientras lo hacía, agregué soporte para devolver fallas intermedias.

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

Puede usar la Retryfunción de esta manera, vuelva a intentarlo 3 veces con un retraso de 10 segundos pero sin cancelación.

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

O, vuelva a intentarlo eternamente cada cinco segundos, a menos que se cancele.

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 puede adivinar, en mi código fuente he sobrecargado la Retryfunción para admitir los diferentes tipos de delegado que deseo usar.

Jodrell
fuente
2

Este método permite reintentos en ciertos tipos de excepción (arroja otros inmediatamente).

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

Ejemplo de uso:

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

Actualización después de 6 años: ahora considero que el enfoque a continuación es bastante malo. Para crear una lógica de reintento, deberíamos considerar usar una biblioteca como Polly.


Mi asyncimplementación del método de reintento:

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

Puntos clave: solía .ConfigureAwait(false);y en su Func<dynamic>lugarFunc<T>

Cihan Uygun
fuente
Esto no proporciona una respuesta a la pregunta. Considere publicar su respuesta como una nueva pregunta, utilizando el botón "Preguntar" en la parte superior de la página, luego publique su propia respuesta a la pregunta para compartir lo que aprendió con la comunidad.
elixenide
Mucho más simple con C # 5.0 que codereview.stackexchange.com/q/55983/54000, pero tal vez CansellactionToken debería inyectarse.
SerG
Hay un problema con esta implementación. Después del reintento final, justo antes de darse por vencido, Task.Delayse llama sin motivo.
HappyNomad
@HappyNomad esta es una respuesta de 6 años y ahora considero que es un enfoque bastante malo para crear una lógica de reintento :)) gracias por la notificación. Actualizaré mi respuesta de acuerdo con esa consideración.
Cihan Uygun
0

O qué tal hacerlo un poco más ordenado ...

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

Creo que, por lo general, debe evitarse lanzar excepciones como mecanismo, a menos que las pase entre límites (como construir una biblioteca que otras personas puedan usar). ¿Por qué no simplemente hacer que el DoSomething()comando regrese truesi fue exitoso o falseno?

EDITAR: Y esto se puede encapsular dentro de una función como otros han sugerido también. El único problema es si no está escribiendo la DoSomething()función usted mismo

Miguel
fuente
77
"Creo que, por lo general, se deben evitar las excepciones de lanzamiento como mecanismo, a menos que se las pase entre límites", estoy totalmente en desacuerdo. ¿Cómo sabe que la persona que llamó verificó su declaración falsa (o peor, nula)? ¿Por qué falló el código? Falso no te dice nada más. ¿Qué pasa si la persona que llama tiene que pasar la falla por la pila? Lea msdn.microsoft.com/en-us/library/ms229014.aspx ; estos son para bibliotecas, pero tienen el mismo sentido para el código interno. Y en un equipo, es probable que otras personas llamen a su código.
TrueWill
0

Tuve la necesidad de pasar algún parámetro a mi método para volver a intentarlo y obtener un valor de resultado; así que necesito una expresión. Construí esta clase que hace el trabajo (está inspirada en la de LBushkin). Puedes usarla así:

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

PD. la solución de LBushkin hace un intento más = D

Paolo Sanchi
fuente
0

Agregaría el siguiente código a la respuesta aceptada

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

Básicamente, el código anterior está haciendo que el Retry clase sea genérica para que pueda pasar el tipo de excepción que desea capturar para volver a intentarlo.

Ahora úselo casi de la misma manera pero especificando el tipo de excepción

Retry<EndpointNotFoundException>.Do(() => SomeFunctionThatCanFail(), TimeSpan.FromSeconds(1));
Juan M. Elosegui
fuente
El bucle for siempre se ejecutará un par de veces (según su retryCount) incluso si el código en el bucle TRY CATCH se ejecutó sin excepciones. Sugeriría establecer retryCount igual a retry var en el bucle de prueba, por lo que el bucle for dejará de pasar por encima.
scre_www
@scre_www Creo que te equivocas. Si actionno lanza, entonces Doregresa breakalejándose del forbucle.
HappyNomad
En cualquier caso, hay un problema con esta implementación. Después del reintento final, justo antes de darse por vencido, Thread.Sleepse llama sin motivo.
HappyNomad
0

Sé que esta respuesta es muy antigua, pero solo quería comentar sobre esto porque me he encontrado con problemas al usar estos mientras, haga, cualquier declaración con contadores.

Con los años me he decidido por un mejor enfoque, creo. Es utilizar algún tipo de agregación de eventos como un "Asunto" de extensiones reactivas o similar. Cuando falla un intento, simplemente publica un evento que dice que el intento falló y que la función del agregador reprograme el evento. Esto le permite mucho más control sobre el reintento sin contaminar la llamada con un montón de bucles de reintento y lo que no. Tampoco estás atando un solo hilo con un montón de hilos.

Brandon
fuente
0

Hazlo simple en C #, Java u otros lenguajes:

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

y úsalo en tu código muy simple:

 void simpleMethod(){
    //some code

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

    //some code    
    }

o puedes usarlo en métodos recursivos:

void recursiveMethod(){
    //some code

    if(ShouldRetryHandler.shouldRetry()){
    recursiveMethod();
     }

    //some code    
    }
Choletski
fuente
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);
    }
Bhaskar
fuente
¿Qué hacer con Request.BadRequest?
Danh
0

Retry helper: una implementación genérica de Java que contiene reintentos de tipo retornable y 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();
    }
divya jain
fuente
0

Aquí hay una versión async/ awaitque agrega excepciones y admite la cancelación.

/// <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 );
}
HappyNomad
fuente
-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);          
      }
}
Mark P Neyer
fuente
Debido a que esta throw;es la única forma en que se termina el ciclo infinito, este método en realidad está implementando "intentar hasta que falle N veces" y no el deseado "intentar hasta Nveces hasta que tenga éxito". Necesita una break;o return;después de la llamada para ThingToTryDelegate();que, de lo contrario, se llame continuamente si nunca falla. Además, esto no se compilará porque el primer parámetro de TryNTimesno tiene nombre. -1.
BACON
-1

He escrito una pequeña clase basada en las respuestas publicadas aquí. Esperemos que ayude a alguien: 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

}
natenho
fuente
-1

He implementado una versión asíncrona de la respuesta aceptada de esta manera, y parece funcionar bien, ¿algún comentario?


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

Y llámalo simplemente así:

var result = await Retry.DoAsync(() => MyAsyncMethod(), TimeSpan.FromSeconds(5), 4);
Kerneels Roos
fuente
Thread.Sleep? Bloquear un hilo niega los beneficios de la asincronía. También estoy bastante seguro de que la Task DoAsync()versión debería aceptar un argumento de tipo Func<Task>.
Theodor Zoulias