Polly CircuitBreakerAsync no funciona como esperaba
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
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
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 deClosedoHalfOpenaOpenonReset: Cuando pasa deHalfOpenaCloseonHalfOpen: Cuando pasa deOpenaHalfOpen
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
- La llamada inicial falla y lanza un
Exception - CB se rompe porque se ha alcanzado el umbral y vuelve a lanzar la excepción
- La política combinada escalará el problema del CB al Retry
- Reintentar maneja
Exception, por eso espera un segundo antes de intentar volver a ejecutar el delegado nuevamente - Reintentar intenta llamar al delegado nuevamente pero falla porque el CB es
Openpor eso queBrokenCircuitExceptionse lanza un - 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) - 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.