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

Resuelto noctonura asked hace 15 años • 30 respuestas

De vez en cuando tengo la necesidad de volver a intentar una operación varias veces antes de darme por vencido. 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 reescribir esto en una función de reintento general como:

TryThreeTimes(DoSomething);

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

noctonura avatar Oct 14 '09 05:10 noctonura
Aceptado

Las declaraciones de captura generales que simplemente reintentan la misma llamada pueden ser peligrosas si se usan como mecanismo general de manejo de excepciones. Dicho esto, aquí hay un contenedor de reintento basado en lambda que puede usar con cualquier método. Elegí factorizar el número de reintentos y el tiempo de espera de reintento como parámetros para tener 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 utilizar 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 avatar Oct 13 '2009 22:10 LBushkin

Deberías probar con Polly . Es una biblioteca .NET escrita por mí que permite a los desarrolladores expresar políticas de manejo de excepciones transitorias como Reintentar, Reintentar para siempre, Esperar y reintentar o Disyuntor de manera fluida.

Ejemplo

Policy
    .Handle<SqlException>(ex => ex.Number == 1205)
    .Or<ArgumentException>(ex => ex.ParamName == "example")
    .WaitAndRetry(3, _ => TimeSpan.FromSeconds(3))
    .Execute(DoSomething);
Michael Wolfenden avatar May 05 '2013 08:05 Michael Wolfenden
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/await:

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 avatar Oct 13 '2009 22:10 Drew Noakes

Posiblemente sea una mala idea. En primer lugar, 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 compagina bien consigo mismo. Por ejemplo:

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

Ahora supongamos que la capa de software reenvía una notificación sobre una falla tres veces cuando falla un paquete.

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

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

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

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

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

Para que no piense que esto es sólo un ejemplo tonto: hemos visto este error en el código del cliente, aunque es mucho, mucho peor de lo que he descrito aquí. En el código de cliente particular, la brecha entre que se produjo la condición de error y que finalmente se informó al usuario fue de varias semanas porque muchas capas reintentaban automáticamente con esperas. Imagínense lo que pasaría si hubiera diez reintentos en lugar de tres .

Por lo general, lo correcto ante una condición de error es informarla inmediatamente 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 avatar Oct 14 '2009 05:10 Eric Lippert

El bloque de aplicación de manejo de fallas transitorias 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 avatar Aug 15 '2012 17:08 Grigori Melnik