I'm just trying out the Polly CircuitBreakerAsync and it's not working as I expect.
What am I doing wrong here? I expect the code below to complete and say the circuit is still closed.
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);
}
}
Fiddle: https://dotnetfiddle.net/unfKsC
Output:
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
Your code works as expected. The circuit breaker itself will not break because you have set the consecutive error count to 3. It means that if you have 3 successive failed calls then it will transition from Closed state to Open. If you try to execute yet another call then it will throw a BrokenCircuitException. In Closed state if an exception has been thrown and the threshold has not been reached then it re-throws the exception.
I always suggest to consider the Circuit Breaker as a proxy. It allows calls if everything works fine. If the consumed sub-system / sub-component seems like malfunctioning then it will prevent further calls to avoid unnecessary load.
When you define a Circuit Breaker policy then you can specify 3 callbacks:
onBreak: When it transitions from Closed or HalfOpen to OpenonReset: When it transitions from HalfOpen to CloseonHalfOpen: When it transitions from Open to HalfOpenThe amended policy declaration:
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}")
);
Let's change the consecutive fail threshold to 1 and let's wrap your ExecuteAsync calls in a try catch:
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);
The output will be the following:
Circuit state before execution: Closed
Break 05.000 : Exception
Circuit state after execution: Open
Exception
As you can see the Circuit Breaker has broken and go from Closed to Open state. It has rethrown your exception.
In order to easily demonstrate when the CB throws BrokenCircuitException I will use a Retry logic around the 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}");
});
This policy will try to re-execute your delegate either when an Exception or when a BrokenCircuitException has been thrown. It does that with 1 second delay between the initial attempt and the first (and only) retry.
Let's combine the two policies and let's amend the ExecuteAsync call:
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);
}
The output will be the following:
Circuit state before execution: Closed
Break 05.000 : Exception
Retry 01.000 : Exception
Circuit state after execution: Open
BrokenCircuitException
ExceptionException that's why it waits a second before it tries to re-execute the delegate againOpen that's why a BrokenCircuitException is thrownBrokenCircuitException instance)catch block.Let's amend the parameters of these policies a bit:
durationOfBreak from 5 seconds to 1.5retryCount from 1 to 2var 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);
}
The output will be the following:
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
I hope this little demo app helped you to better understand how does Circuit Breaker work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With