En posts pasados hemos estado hablando sobr elas excepciones en C#, desde cómo funcionan hasta cómo evitarlas... sin embargo, hay ocasiones en las que llegue a ser absolutamente necesario que la operación que provocó la excepción se repita, ya sea porque es de suma importancia que esta se lleve a cabo o porque el error es solo temporal.

El NuGet del que les voy a hablar hoy nos ayuda a implementar políticas de recuperación ante una falla. Esto es particularmente útil cuando, por ejemplo, estamos consumiendo un servicio web y una petición falla.

Policy

Todo comienza estableciendo una política con la clase Policy a través de su api fluída:

var politicaReintenta5 = Policy // continúa

Posteriormente se indica qué errores se tienen que manejar, así que para ello usamos el método Handle:

    .Handle<ArgumentException>()

En este caso se está indicando que se quiere manejar las excepciones del tipo ArgumentException (podrías haber especificado en su lugar Exception aunque tal vez no sea lo ideal, como vimos también antes), adicionalmente puedes especificar otras excepciones usando Or:

    .Or<DivideByZeroException>()

Para posteriormente indicar la acción que se debe realizar, la más común es la de reintentar Retry:

    .Retry(5, ReportaError);

El método Retry permite especificar el número de veces que se debe reintentar la operación (en este caso 5) y un método (ReportaError) que se llamará cada vez que se vaya a reintentar.

El método ReportaError

En este caso, el método recibe la excepción lanzada y el número de intento en el que está. Imprime el número de intento y la hora en la que se está ejecutando:

static void ReportaError(Exception e, int intentos)
{
    Console.WriteLine($"Intento: {intentos:00}\tTiempo: {DateTime.Now}\nError: {e.Message}");
}

Por cierto, no es necesario que especifiques una función, yo lo hice solo para este demo.

Ejecutando la política

Una vez que has decidido ya la política, es momento de ejecutar el código que podría fallar, toma como ejemplo el siguiente método:

static void LanzaExcepcion()
{
    throw new DivideByZeroException();
}

Sí, únicamente lanza una excepción, pero podría ser cualquier otra cosa: una conexión a un servicio web, una operación matemática... cualquier cosa.

Entonces para ejecutarlo tomamos la póliza de ejecución (que en realidad es RetryPolicy) y con su método Execute, el cual recibe un tipo Action, dentro del cual vamos a ejecutar el código "peligroso":

try
{
    politicaReintenta5.Execute(() => 
    { 
        LanzaExcepcion(); 
    });
}
catch(Exception e) 
{ 
    Console.WriteLine($"Después de los intentos, sigue fallando ({e.Message})");
}

Oh, por cierto, usando la póliza de Retry (reintentar) no te libras de tener que manejar la excepción tu mismo puesto que al terminar los reintentos, si la acción no se pudo ejecutar la excepción será lanzada como originalmente lo haría. En lo que nos ayuda Polly en este caso es a programar automáticamente los reintentos. Habiendo dicho esto, si ejecutamos el código anterior, esto es lo que obtendremos en la consola:

Intento: 01	Tiempo: 6/30/2017 1:37:57 PM
Intento: 02	Tiempo: 6/30/2017 1:37:57 PM
Intento: 03	Tiempo: 6/30/2017 1:37:57 PM
Intento: 04	Tiempo: 6/30/2017 1:37:57 PM
Intento: 05	Tiempo: 6/30/2017 1:37:57 PM
Después de los intentos, sigue fallando (Attempted to divide by zero.)

Reintentar esperando entre reintentos

Como puedes ver, los reintentos son inmediatos, pero esto podría no ser siempre lo ideal, ¿si un servicio web no me respondió hace 30 milésimas de segundo, por qué lo hará ahora?

Para estos casos, Polly ofrece la política de "esperar y reintentar":

var politicaWaitAndRetry = Policy
    .Handle<DivideByZeroException>()
    .WaitAndRetry(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(2),
            TimeSpan.FromSeconds(3)
        }, ReportaError);

Lo nuevo está a partir del método WaitAndRetry. Este método también tiene diversas sobrecargas, pero una de ellas recibe un arreglo de TimeSpan, que le indica cuánto tiempo debe esperar entre reintentos. En este caso intentará tres veces, esperando 1, 2 y tres segundos entre ellos.

El método ReportarError

En este caso el método que se llama a cada reintento es un poco distinto, recibe la excepción,un TimeSpan indicando el tiempo a esperar, el número de intentos y un objeto del tipo Context que es para usos más avanzados de Polly. Lo que hace es imprimir el número de intento y el tiempo que tardará en realizarse la próxima ejecución:

static void ReportaError(Exception e, TimeSpan tiempo, int intento, Context contexto)
{
    Console.WriteLine($"Intento: {intento:00} (próximo intento en: {tiempo.Seconds} segundos)\tTiempo: {DateTime.Now}");
}

Para hacer uso de la política de nuevo, usamos Execute:

try
{
    politicaWaitAndRetry.Execute(() => 
    { 
        LanzaExcepcion(); 
    });
}
catch(Exception e) 
{ 
    Console.WriteLine($"Después de los intentos, sigue fallando ({e.Message})");
}

De nueva cuenta, la ejecución está envuelta en un bloque try por si después de todos los intentos el código sigue fallando. El resultado de ejecutar el código es el siguiente:

Intento: 01 (próximo intento en: 1 segundos)	Tiempo: 6/30/2017 1:37:57 PM
Intento: 02 (próximo intento en: 2 segundos)	Tiempo: 6/30/2017 1:37:58 PM
Intento: 03 (próximo intento en: 3 segundos)	Tiempo: 6/30/2017 1:38:00 PM
Después de los intentos, sigue fallando (Attempted to divide by zero.)

Una de las prácticas más comunes es la de ir aumentando el tiempo de espera para reintentar una operación de forma exponencial, la forma de implementar esta técnica en Polly es a través de una sobrecarga de WaitAndRetry, que recibe un entero indicando el número de intentos y una Func<int, TimeSpan> para definir el tiempo de espera:

Intento: 01 (próximo intento en: 2 segundos)	Tiempo: 6/30/2017 1:38:03 PM
Intento: 02 (próximo intento en: 4 segundos)	Tiempo: 6/30/2017 1:38:05 PM
Intento: 03 (próximo intento en: 8 segundos)	Tiempo: 6/30/2017 1:38:09 PM
Intento: 04 (próximo intento en: 16 segundos)	Tiempo: 6/30/2017 1:38:17 PM
Intento: 05 (próximo intento en: 32 segundos)	Tiempo: 6/30/2017 1:38:33 PM
Después de los intentos, sigue fallando (Attempted to divide by zero.)

Si fallla...

Hasta ahora habíamos tenido que envolver la ejecución en un bloque try, sin embargo, esto se puede evitar utilizando la política de Fallback, esta permite establecer una acción que debe realizarse en caso de que todo falle. Ojo que esta no es compatible con la política Retry directamente, se pueden mezclar de otra forma que veremos más adelante.

Ahora vamos a introducir otra modificación, hasta ahora habíamos trabajado con métodos "peligrosos" que no regresaban ningún valor, pero esto no es lo que regularmente harás, las llamadas a servicios web regularmente retornan valores, y es muy probable que eso es lo que quieras hacer en tu código, toma por ejemplo este código, que podría (en este caso siempre) lanzar una excepción pero que idealmente regresa una cadena:

static string LanzaExcepcionConCadena()
{
    throw new Exception();
    return "Hola";
}

Para usarlo junto con una de las políticas de Polly nuevamente hacemos uso de la clase Policy, pero ahora en su versión genérica:

var politicaWithFallback = Policy<string>
    .Handle<Exception>()

Estamos relacionando a nuestra política con el tipo de dato string e indicándole que debe manejar cualquier tipo de Exception, pero lo nuevo es lo siguiente:

    .Fallback("Valor de Fallback");

El método Fallback indica otra política, una que nos permite establecer un valor por default, en caso de que la ejecución falle. En el código anterior se le indica que la cadena "Valor de Fallback" será devuelta en caso de que la ejecución falle. Entonces podemos llamar a ejecutar el código así:

var resultado2 = politicaWithFallback.Execute(() =>
{
    return LanzaExcepcionConCadena();
});
Console.WriteLine($"Resultado: {resultado2}");

Y obtendremos el siguiente resultado:

  
Resultado: Valor de Fallback

Uniendo políticas

Pero vamos, que estas políticas son buenas por si mismas, ahora, imagínatelas combinadas... es decir, que tu código intente 5 veces conseguir un valor y si no lo consigue, que tome uno por default. Esto es posible "envolviendo" las políticas mediante el método Wrap:

var mixedPolicy = Policy.Wrap(politicaWithFallback, politicaWaitAndRetryString);

Dentro de mixedPolicy están juntas tanto la política de esperar y reintentar como la de asignar un valor por default. Las políticas se ejecutarán de derecha a izquierda, entonces, al ejecutar el siguiente código

var resultado3 = mixedPolicy.Execute(LanzaExcepcionConCadena);
Console.WriteLine($"Resultado: {resultado3}");

En la pantalla se mostrará lo siguiente:

Intento: 01 (próximo intento en: 2 segundos)	Tiempo: 6/30/2017 9:33:46 PM
Intento: 02 (próximo intento en: 4 segundos)	Tiempo: 6/30/2017 9:33:48 PM
Intento: 03 (próximo intento en: 8 segundos)	Tiempo: 6/30/2017 9:33:52 PM
Intento: 04 (próximo intento en: 16 segundos)	Tiempo: 6/30/2017 9:34:00 PM
Intento: 05 (próximo intento en: 32 segundos)	Tiempo: 6/30/2017 9:34:16 PM
Resultado: Valor de Fallback

Más características

Esta biblioteca tiene más características que no cubrí en este post, te invito a que descargues el código de ejemplo de este post para que pruebes todas las posibilidades.

Uso

Puedes acceder a Polly a través de este paquete de NuGet.

PM> Install-Package Polly

Polly es parte de la .NET Foundation, y su código está disponible en GitHub, puedes ver cómo está hecho y contribuir a mejorarlo.