Polly CircuitBreakerAsync no funciona como esperaba

Nov 17 2020

Solo estoy probando Polly CircuitBreakerAsync y no funciona como esperaba.

¿Qué estoy haciendo mal aquí? Espero que el siguiente código se complete y diga que el circuito aún está cerrado.

using Polly; 
using System;
using System.Threading.Tasks;

public class Program
{
    public static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
    }
    
    static async Task MainAsync(string[] args)
    {
        var circuitBreaker = Policy
            .Handle<Exception>()
            .CircuitBreakerAsync(
                3, // ConsecutiveExceptionsAllowedBeforeBreaking,
                TimeSpan.FromSeconds(5) // DurationOfBreak
            );

        Console.WriteLine("Circuit state before execution: " + circuitBreaker.CircuitState);

        await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
        await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
        await circuitBreaker.ExecuteAsync(() => { throw new System.Exception(); });
        await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
        await circuitBreaker.ExecuteAsync(() => Task.Delay(25));

        Console.WriteLine("Circuit state after execution: " + circuitBreaker.CircuitState);
    }
}

Violín: https://dotnetfiddle.net/unfKsC

Salida:

Circuit state before execution: Closed
Run-time exception (line 25): Exception of type 'System.Exception' was thrown.

Stack Trace:

[System.Exception: Exception of type 'System.Exception' was thrown.]
   at Program.<MainAsync>b__2() :line 25
   at Polly.Policy.<>c__DisplayClass116_0.<ExecuteAsync>b__0(Context ctx, CancellationToken ct)
   at Polly.CircuitBreakerSyntaxAsync.<>c__DisplayClass4_1.<<CircuitBreakerAsync>b__2>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Polly.CircuitBreaker.CircuitBreakerEngine.<ImplementationAsync>d__1`1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Polly.Policy.<ExecuteAsync>d__135.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at Program.<MainAsync>d__a.MoveNext() :line 25
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at Program.Main(String[] args) :line 9

Respuestas

3 00110001 Nov 17 2020 at 05:53

Esto está funcionando como se esperaba

https://github.com/App-vNext/Polly/wiki/Circuit-Breaker

Manejo de excepciones

Un interruptor existe como un dispositivo de medición y corte: para medir las excepciones manejadas lanzadas por las acciones que usted realiza a través de él y para romper cuando se excede el umbral de falla configurado.

  • Un interruptor automático no organiza reintentos.
  • Un interruptor automático no absorbe excepciones (a diferencia de los reintentos). Todas las excepciones generadas por acciones ejecutadas a través de la política (ambas excepciones manejadas por la política y no) se vuelven a generar intencionalmente . Excepciones manejadas por las métricas de actualización de políticas que rigen el estado del circuito; las excepciones no manejadas por la política no lo hacen.

En resumen, no maneja sus excepciones, las vuelve a lanzar

4 PeterCsala Nov 17 2020 at 15:47

Disyuntor en general

Tu código funciona como se esperaba. El disyuntor en sí no se romperá porque ha establecido el recuento de errores consecutivos en 3. Significa que si tiene 3 llamadas fallidas sucesivas, pasará del Closedestado al Open. Si intenta ejecutar otra llamada, lanzará un BrokenCircuitException. En el Closedestado, si se ha lanzado una excepción y no se ha alcanzado el umbral, vuelve a lanzar la excepción.

Siempre sugiero considerar el disyuntor como un proxy. Permite llamadas si todo funciona bien. Si el subsistema / subcomponente consumido parece estar funcionando mal, evitará más llamadas para evitar una carga innecesaria.

Funciones de devolución de llamada para depuración

Cuando define una política de disyuntor, puede especificar 3 devoluciones de llamada:

  • onBreak: Cuando pasa de Closedo HalfOpenaOpen
  • onReset: Cuando pasa de HalfOpenaClose
  • onHalfOpen: Cuando pasa de OpenaHalfOpen

La declaración de política modificada:

var circuitBreaker = Policy
    .Handle<Exception>()
    .CircuitBreakerAsync(3, TimeSpan.FromSeconds(5), 
        onBreak: (ex, @break) => Console.WriteLine($"{"Break",-10}{@break,-10:ss\\.fff}: {ex.GetType().Name}"), onReset: () => Console.WriteLine($"{"Reset",-10}"),
        onHalfOpen: () => Console.WriteLine($"{"HalfOpen",-10}")
    );

Recuento sucesivo de fallos

Cambiemos el umbral de error consecutivo a 1 y envolvemos sus ExecuteAsyncllamadas en una captura de prueba:

var circuitBreaker = Policy
    .Handle<Exception>()
    .CircuitBreakerAsync(1, TimeSpan.FromSeconds(5), 
        onBreak: (ex, @break) => Console.WriteLine($"{"Break",-10}{@break,-10:ss\\.fff}: {ex.GetType().Name}"),
        onReset: () => Console.WriteLine($"{"Reset",-10}"), onHalfOpen: () => Console.WriteLine($"{"HalfOpen",-10}")
    );
Console.WriteLine("Circuit state before execution: " + circuitBreaker.CircuitState);

try
{
    await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
    await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
    await circuitBreaker.ExecuteAsync(() => { throw new System.Exception(); });
    await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
    await circuitBreaker.ExecuteAsync(() => Task.Delay(25));
}
catch (Exception ex)
{
    Console.WriteLine("Circuit state after execution: " + circuitBreaker.CircuitState);
    Console.WriteLine(ex.GetType().Name);
}

Console.WriteLine("Circuit state after execution: " + circuitBreaker.CircuitState);

La salida será la siguiente:

Circuit state before execution: Closed
Break     05.000    : Exception
Circuit state after execution: Open
Exception

Como puede ver, el disyuntor se ha roto y pasa de Closedun Openestado a otro. Ha vuelto a lanzar tu excepción.

Combinar reintento y disyuntor

Para demostrar fácilmente cuándo lanza el CB BrokenCircuitException, usaré una lógica de reintento alrededor del CB.

var retry = Policy
    .Handle<Exception>()
    .Or<BrokenCircuitException>()
    .WaitAndRetryAsync(
        retryCount: 1,
        sleepDurationProvider: _ => TimeSpan.FromSeconds(1),
        onRetry: (exception, delay, context) =>
        {
            Console.WriteLine($"{"Retry",-10}{delay,-10:ss\\.fff}: {exception.GetType().Name}");
        });

Esta política intentará volver a ejecutar a su delegado Exceptioncuando BrokenCircuitExceptionse haya lanzado un o cuando se haya lanzado. Lo hace con 1 segundo de retraso entre el intento inicial y el primer (y único) reintento.

Combinemos las dos políticas y modifiquemos la ExecuteAsyncllamada:

var strategy = Policy.WrapAsync(retry, circuitBreaker);
try
{
    await strategy.ExecuteAsync(() => { throw new System.Exception(); });
}
catch (Exception ex)
{
    Console.WriteLine("Circuit state after execution: " + circuitBreaker.CircuitState);
    Console.WriteLine(ex.GetType().Name);
}

La salida será la siguiente:

Circuit state before execution: Closed
Break     05.000    : Exception
Retry     01.000    : Exception
Circuit state after execution: Open
BrokenCircuitException
  1. La llamada inicial falla y lanza un Exception
  2. CB se rompe porque se ha alcanzado el umbral y vuelve a lanzar la excepción
  3. La política combinada escalará el problema del CB al Retry
  4. Reintentar maneja Exception, por eso espera un segundo antes de intentar volver a ejecutar el delegado nuevamente
  5. Reintentar intenta llamar al delegado nuevamente pero falla porque el CB es Openpor eso que BrokenCircuitExceptionse lanza un
  6. Debido a que no hay más reintentos, es por eso que la política de reintentos volverá a lanzar su excepción (que ahora es una BrokenCircuitExceptioninstancia)
  7. Esa excepción es atrapada por nuestro catchbloque.

Ejemplo perfeccionado

Modifiquemos un poco los parámetros de estas políticas:

  • CB durationOfBreakde 5 segundos a 1,5
  • Reintentos retryCountde 1 a 2
var retry = Policy
    .Handle<Exception>()
    .Or<BrokenCircuitException>()
    .WaitAndRetryAsync(2, _ => TimeSpan.FromSeconds(1),
        onRetry: (exception, delay, context) =>
        {
            Console.WriteLine($"{"Retry",-10}{delay,-10:ss\\.fff}: {exception.GetType().Name}");
        });

var circuitBreaker = Policy
    .Handle<Exception>()
    .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1500),
        onBreak: (ex, @break) => Console.WriteLine($"{"Break",-10}{@break,-10:ss\\.fff}: {ex.GetType().Name}"), onReset: () => Console.WriteLine($"{"Reset",-10}"),
        onHalfOpen: () => Console.WriteLine($"{"HalfOpen",-10}")
    );

Console.WriteLine("Circuit state before execution: " + circuitBreaker.CircuitState);

var strategy = Policy.WrapAsync(retry, circuitBreaker);
try
{
    await strategy.ExecuteAsync(() => { throw new System.Exception(); });
}
catch (Exception ex)
{
    Console.WriteLine("Circuit state after execution: " + circuitBreaker.CircuitState);
    Console.WriteLine(ex.GetType().Name);
}

La salida será la siguiente:

Circuit state before execution: Closed
Break     01.500    : Exception
Retry     01.000    : Exception
Retry     01.000    : BrokenCircuitException
HalfOpen
Break     01.500    : Exception
Circuit state after execution: Open
Exception

Espero que esta pequeña aplicación de demostración te haya ayudado a comprender mejor cómo funciona el interruptor automático.