2

I want to use Polly to implement a Circuit Breaker pattern.

In the docs, there is a description of the Half Open state, and there it says:

  • If a handled exception is received, that exception is rethrown, and the circuit transitions immediately back to open, and remains open again for the configured timespan.
  • If an unhandled exception is received, the circuit remains in half-open.

I'm not sure I understand the difference here between handled and unhandled exception. We are describing a case where an action is run by the policy and is throwing an exception.

When they say the exception is handled, where do they mean it's being handled? because as we said, the action threw it so doesn't it mean it's unhandled?

It makes me not understand completely when the half open state remains half open and when does it transition to open.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
CodeMonkey
  • 11,196
  • 30
  • 112
  • 203
  • Please, share a [mcve] – Pavel Anikhouski Nov 24 '20 at 10:20
  • 2
    There is no example and i did not write any code. I'm asking about the documentation – CodeMonkey Nov 24 '20 at 10:34
  • @YonatanNir I've described how does Circuit Breaker works through a sample application in [this answer](https://stackoverflow.com/a/64871939/13268855). Please check it and if you have further questions let's discuss it. – Peter Csala Nov 24 '20 at 13:31
  • @PeterCsala Your answer was great and I do get how CB works, but it still didn't answer my question here.. I'm still not sure of the difference between handled and unhandled exception here – CodeMonkey Nov 25 '20 at 07:51

1 Answers1

3

When you define a Circuit Breaker policy then you can define what sort of exception(s) should be considered by the CB implementation. In other words you can list those exceptions that should be treated as failed execution and should be counted into the successive failure count.

You can define the list of exceptions with the combination of Handle<T> and Or<T> method calls.

Let's scrutinize this concept via a simple example:

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

var circuitBreaker = Policy
    .Handle<ArgumentException>()
    .CircuitBreaker(2, TimeSpan.FromSeconds(1),
        onBreak: (ex, @break) => Console.WriteLine($"{"Break",-10}{@break,-10:ss\\.fff}: {ex.GetType().Name}"),
        onReset: () => Console.WriteLine($"{"Reset",-10}"),
        onHalfOpen: () => Console.WriteLine($"{"HalfOpen",-10}"));
  • The circuit breaker policy considers all ArgumentExceptions (including ArgumentNullException and ArgumentOutOfRangeException) as handled exception.
    • This means that if the called delegate throws one of these three exceptions then it will increase the successive failure count and if the threshold is reached then it will break.
  • The retry policy is triggered in case of ArgumentException and in case of NotSupportedException as well.
    • If either of these is thrown then it will sleep for a second and then it tries to re-execute the same delegate.

So, from the Circuit Breaker perspective if a NotSupportedException is thrown than it will not be considered >> hence the name unhandled.

This is how our sample method is implemented which will either throw an ArgumentException or a NotSupportedException:

private static int count = 0;
private const int threshold = 3;
static void SampleCall()
{
    count++;
    if (count >= threshold) throw new NotSupportedException();
    throw new ArgumentException("Nothing");
}

The usage of the policies:

var strategy = Policy.Wrap(retry, circuitBreaker);

try
{
    strategy.Execute(SampleCall);
    Console.WriteLine("Succeeded");
}
catch (NotSupportedException)
{
    Console.WriteLine("Failed");
}

Output when threshold is set to 3

Retry     01.000    : ArgumentException
Break     01.000    : ArgumentException
Retry     01.000    : ArgumentException
HalfOpen
Retry     01.000    : NotSupportedException
Retry     01.000    : NotSupportedException
Retry     01.000    : NotSupportedException
Failed

After the CB has been transferred itself into the HalfOpen state then the SampleCall throws only NotSupportedExceptions. This is not handled by the CB that's why it remains in the HalfOpen state.

Output when threshold is set to 2

Retry     01.000    : ArgumentException
Retry     01.000    : NotSupportedException
Retry     01.000    : NotSupportedException
Retry     01.000    : NotSupportedException
Retry     01.000    : NotSupportedException
Failed

The CB did not break because there was no two successive ArgumentException. But the retry did trigger because it also handles NotSupportedException.

Output when threshold is set to 4

Retry     01.000    : ArgumentException
Break     01.000    : ArgumentException
Retry     01.000    : ArgumentException
HalfOpen
Break     01.000    : ArgumentException
Retry     01.000    : ArgumentException
HalfOpen
Retry     01.000    : NotSupportedException
Retry     01.000    : NotSupportedException
Failed

Because the SampleCall did throw ArgumentException when the CB was in the HalfOpen state that's why CB considered that as handled exception and transferred itself from HalfOpen to Open.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • About threshold = 2, why wouldn't the CB break? There's the first original call and then the first retry is the 2nd time throwing the exception – CodeMonkey Nov 25 '20 at 23:26
  • @YonatanNir 1) `SampleCall`'s initial attempt 2) `count` is increased from 0 to 1 3) `threshold` is checked 4) `ArgumentEx` is thrown 5) CB's failure count is increased from 0 to 1 6) Retry is triggered 7) Retry sleeps 8) `SampleCall`'s first retry attempt 9) `count` is increased from 1 to 2 10) `threshold` is checked 11) `NotSupportedEx` is thrown 11) CB is skipped because of unhandled exception 12) Retry is triggered 13)... – Peter Csala Nov 26 '20 at 06:47
  • 1
    ahh ok ok.. I did it all in my head so i checked first and only increased later. Another question if I may.. the order of policies inside Policy.Wrap does matter right? the parameter on the most right is the policy which will be triggered first? – CodeMonkey Nov 26 '20 at 11:19
  • @YonatanNir Yes the ordering does matter. The `Policy.Wrap` could be considered as an [escalation chain](https://github.com/peter-csala/resilience-service-design/blob/main/resilience.md#esc). If the inner can't handle then it will be propagated to the outer. The one on the most right end is the most inner policy and the one on the most left end is the most outer policy. For example, if you have `Wrap(retry, timeout)` that means for each attempt there is a timeout constraint. On the other hand if you have `Wrap(timeout, retry)` that means you have a global timeout for the sum of the attempts. – Peter Csala Nov 26 '20 at 11:44
  • another small question.. the docs says "The next action will be treated as a trial, to determine the circuit's health: the action delegate passed to the .Execute(...) call will be attempted. (One additional attempt will be permitted per durationOfBreak.. ). Does it mean there are actually 2 calls as a test or 1? if there are 2 then its enough that just one of them is successful that the circuit will close? – CodeMonkey Dec 03 '20 at 13:24
  • @YonatanNir The documentation is kinda clumsy here. The next action means the subsequent request, which may occur 1 second or 1 hour later. There is no automatic transition (for example after a given period) from `Half-Open` to any other state. It can be triggered only by a subsequent request. – Peter Csala Dec 03 '20 at 14:15
  • @YonatanNir The *One additional attempt will be permitted per durationOfBreak* part is not clear for me to be honest. Because during that period the CB stays in `Open` state, not in `Half-Open`. My best guess is that if the CB has been broken and you emit a new request during the `durationOfBreak` period then it is treated as a trial / probe and it can transition you back to `Close` state if the request succeeded. But that's just an educated guess. – Peter Csala Dec 03 '20 at 14:20