Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculated break duration for Circuit breaker #1776

Merged
merged 40 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9d45467
Add BreakDurationGenerator
atawLee Oct 22, 2023
4880911
Add Duration Generator UnitTest
atawLee Oct 22, 2023
5752bb6
Apply suggestions from code review
atawLee Oct 23, 2023
9e2e5f2
Merge branch 'main' into DurationGenerator
jognhoonlee Oct 26, 2023
22e2590
Add BreakDurationGeneratorArguments
atawLee Oct 26, 2023
29942be
Update circuit-breaker.md
atawLee Oct 26, 2023
030b234
Update circuit-breaker.md
atawLee Oct 26, 2023
b924728
remove duplicate
atawLee Oct 26, 2023
8f9a594
Add BreakDurationGenerator
atawLee Oct 22, 2023
b9023aa
Add Duration Generator UnitTest
atawLee Oct 22, 2023
31ff325
Apply suggestions from code review
atawLee Oct 23, 2023
2faadbd
Add BreakDurationGeneratorArguments
atawLee Oct 26, 2023
491c99f
Update document circuit-breaker.md
atawLee Oct 26, 2023
f071d23
Merge branch 'DurationGenerator' of https://github.com/atawLee/Polly …
atawLee Oct 26, 2023
9d2ae69
fix document
atawLee Oct 26, 2023
b6a8948
Update Document
atawLee Oct 27, 2023
a1c8474
Update CircuitBreaker Document
atawLee Oct 27, 2023
92ec473
Merge branch 'App-vNext:main' into DurationGenerator
atawLee Oct 27, 2023
e24ab51
Apply suggestions from code review
atawLee Oct 29, 2023
39c4721
Apply suggestions from code review
atawLee Oct 29, 2023
5f7095d
Merge branch 'App-vNext:main' into DurationGenerator
atawLee Oct 30, 2023
51620d7
Merge branch 'App-vNext:main' into DurationGenerator
atawLee Oct 30, 2023
92de086
update document
atawLee Oct 30, 2023
3685e9d
Merge branch 'DurationGenerator' of https://github.com/atawLee/Polly …
atawLee Oct 30, 2023
8e5f803
controller - Add context
atawLee Oct 30, 2023
8a82bf0
update unittest
atawLee Oct 30, 2023
cf02659
Apply suggestions from code review
atawLee Nov 2, 2023
f55b6aa
Merge branch 'App-vNext:main' into DurationGenerator
atawLee Nov 2, 2023
efcb36b
Reflect 'ValueTask' requirements
atawLee Nov 2, 2023
fc80c3e
Fixed warning
atawLee Nov 2, 2023
5b1a882
Fixed Warning
atawLee Nov 2, 2023
6962659
update PublishAPI
atawLee Nov 2, 2023
5355b3e
Fixed Unittest
atawLee Nov 2, 2023
5448c32
Delete Korean Comment
atawLee Nov 2, 2023
1e32edd
Pull MainBranch
atawLee Nov 6, 2023
e4c3c7b
Pull Main Branch
atawLee Nov 6, 2023
7044ed4
Merge Main Branch
atawLee Nov 6, 2023
8867436
Update Test Coverage
atawLee Nov 7, 2023
fb81ee6
Apply suggestions from code review
atawLee Nov 7, 2023
01b35fc
#pragma warning disable S1226
atawLee Nov 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Merge Main Branch
  • Loading branch information
atawLee committed Nov 6, 2023
commit 7044ed448b8df81aac82fcbd06c0c36c9a59b0a7
13 changes: 0 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,19 +229,6 @@ var optionsComplex = new CircuitBreakerStrategyOptions
ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>()
};

// Adds a circuit breaker with a dynamic break duration:
//
// Same circuit breaking conditions as above, but with a dynamic break duration based on the failure count.
new ResiliencePipelineBuilder().AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 8,
BreakDuration = TimeSpan.FromSeconds(30),
BreakDurationGenerator = static args => TimeSpan.FromSeconds(Math.Min(20 + Math.Pow(2, args.FailureCount), 400)),
ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(),
});

// Handle specific failed results for HttpResponseMessage:
var optionsShouldHandle = new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
Expand Down
196 changes: 165 additions & 31 deletions docs/strategies/circuit-breaker.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,6 @@ var optionsComplex = new CircuitBreakerStrategyOptions
ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>()
};

// Adds a circuit breaker with a dynamic break duration:
//
// Same circuit breaking conditions as above, but with a dynamic break duration based on the failure count.
new ResiliencePipelineBuilder().AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 8,
BreakDuration = TimeSpan.FromSeconds(30),
BreakDurationGenerator = static args => TimeSpan.FromSeconds(Math.Min(20 + Math.Pow(2, args.FailureCount), 400)),
ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(),
});

// Handle specific failed results for HttpResponseMessage:
var optionsShouldHandle = new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
Expand Down Expand Up @@ -94,19 +81,18 @@ new ResiliencePipelineBuilder<HttpResponseMessage>().AddCircuitBreaker(optionsSt

## Defaults

| Property | Default Value | Description |
| ----------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `ShouldHandle` | Predicate that handles all exceptions except `OperationCanceledException`. | Specifies which results and exceptions are managed by the circuit breaker strategy. |
| `FailureRatio` | 0.1 | The ratio of failures to successes that will cause the circuit to break/open. |
| `MinimumThroughput` | 100 | The minimum number of actions that must occur in the circuit within a specific time slice. |
| `SamplingDuration` | 30 seconds | The time period over which failure ratios are calculated. |
| `BreakDuration` | 5 seconds | The time period for which the circuit will remain broken/open before attempting to reset. |
| `BreakDurationGenerator`| `null` | A function to dynamically generate the break duration based on certain parameters. |
| `OnClosed` | `null` | Event triggered when the circuit transitions to the `Closed` state. |
| `OnOpened` | `null` | Event triggered when the circuit transitions to the `Opened` state. |
| `OnHalfOpened` | `null` | Event triggered when the circuit transitions to the `HalfOpened` state. |
| `ManualControl` | `null` | Allows for manual control to isolate or close the circuit. |
| `StateProvider` | `null` | Enables the retrieval of the current state of the circuit. |
| Property | Default Value | Description |
| ------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `ShouldHandle` | Predicate that handles all exceptions except `OperationCanceledException`. | Specifies which results and exceptions are managed by the circuit breaker strategy. |
| `FailureRatio` | 0.1 | The ratio of failures to successes that will cause the circuit to break/open. |
| `MinimumThroughput` | 100 | The minimum number of actions that must occur in the circuit within a specific time slice. |
| `SamplingDuration` | 30 seconds | The time period over which failure ratios are calculated. |
| `BreakDuration` | 5 seconds | The time period for which the circuit will remain broken/open before attempting to reset. |
| `OnClosed` | `null` | Event triggered when the circuit transitions to the `Closed` state. |
| `OnOpened` | `null` | Event triggered when the circuit transitions to the `Opened` state. |
| `OnHalfOpened` | `null` | Event triggered when the circuit transitions to the `HalfOpened` state. |
| `ManualControl` | `null` | Allows for manual control to isolate or close the circuit. |
| `StateProvider` | `null` | Enables the retrieval of the current state of the circuit. |

## Diagrams

Expand Down Expand Up @@ -322,7 +308,7 @@ sequenceDiagram

Over the years, many developers have used Polly in various ways. Some of these recurring patterns may not be ideal. This section highlights the recommended practices and those to avoid.

### 1 - Using different sleep duration between retry attempts based on Circuit Breaker state
### Using different sleep duration between retry attempts based on Circuit Breaker state

Imagine that we have an inner Circuit Breaker and an outer Retry strategies.

Expand All @@ -332,7 +318,7 @@ We would like to define the retry in a way that the sleep duration calculation i

Use a closure to branch based on circuit breaker state:

<!-- snippet: circuit-breaker-anti-pattern-1 -->
<!-- snippet: circuit-breaker-anti-pattern-circuit-aware-retry -->
```cs
var stateProvider = new CircuitBreakerStateProvider();
var circuitBreaker = new ResiliencePipelineBuilder()
Expand Down Expand Up @@ -376,7 +362,7 @@ var retry = new ResiliencePipelineBuilder()

Use `Context` to pass information between strategies:

<!-- snippet: circuit-breaker-pattern-1 -->
<!-- snippet: circuit-breaker-pattern-circuit-aware-retry -->
```cs
var circuitBreaker = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new()
Expand Down Expand Up @@ -420,7 +406,73 @@ var retry = new ResiliencePipelineBuilder()
- The Retry strategy fetches the sleep duration dynamically without knowing any specific knowledge about the Circuit Breaker.
- If adjustments are needed for the `BreakDuration`, they can be made in one place.

### 2 - Wrapping each endpoint with a circuit breaker
### Using different duration for breaks

In the case of Retry you can specify dynamically the sleep duration via the `DelayGenerator`.

In the case of Circuit Breaker the `BreakDuration` is considered constant (can't be changed between breaks).

❌ DON'T

Use `Task.Delay` inside `OnOpened`:

<!-- snippet: circuit-breaker-anti-pattern-sleep-duration-generator -->
```cs
static IEnumerable<TimeSpan> GetSleepDuration()
{
for (int i = 1; i < 10; i++)
{
yield return TimeSpan.FromSeconds(i);
}
}

var sleepDurationProvider = GetSleepDuration().GetEnumerator();
sleepDurationProvider.MoveNext();

var circuitBreaker = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new()
{
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
BreakDuration = TimeSpan.FromSeconds(0.5),
OnOpened = async args =>
{
await Task.Delay(sleepDurationProvider.Current);
sleepDurationProvider.MoveNext();
}

})
.Build();
```
<!-- endSnippet -->

**Reasoning**:

- The minimum break duration value is half a second. This implies that each sleep lasts for `sleepDurationProvider.Current` plus an additional half a second.
- One might think that setting the `BreakDuration` to `sleepDurationProvider.Current` would address this, but it doesn't. This is because the `BreakDuration` is established only once and isn't re-assessed during each break.

<!-- snippet: circuit-breaker-anti-pattern-sleep-duration-generator-ext -->
```cs
circuitBreaker = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new()
{
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
BreakDuration = sleepDurationProvider.Current,
OnOpened = async args =>
{
Console.WriteLine($"Break: {sleepDurationProvider.Current}");
sleepDurationProvider.MoveNext();
}

})
.Build();
```
<!-- endSnippet -->

✅ DO

The `CircuitBreakerStrategyOptions` currently do not support defining break durations dynamically. This may be re-evaluated in the future. For now, refer to the first example for a potential workaround. However, please use it with caution.

### Wrapping each endpoint with a circuit breaker

Imagine that you have to call N number of services via `HttpClient`s.
You want to decorate all downstream calls with the service-aware Circuit Breaker.
Expand All @@ -429,7 +481,7 @@ You want to decorate all downstream calls with the service-aware Circuit Breaker

Use a collection of Circuit Breakers and explicitly call `ExecuteAsync()`:

<!-- snippet: circuit-breaker-anti-pattern-2 -->
<!-- snippet: circuit-breaker-anti-pattern-cb-per-endpoint -->
```cs
// Defined in a common place
var uriToCbMappings = new Dictionary<Uri, ResiliencePipeline>
Expand Down Expand Up @@ -483,3 +535,85 @@ public Downstream1Client(
> The above sample code used the `AsAsyncPolicy<HttpResponseMessage>()` method to convert the `ResiliencePipeline<HttpResponseMessage>` to `IAsyncPolicy<HttpResponseMessage>`.
> It is required because the `AddPolicyHandler()` method anticipates an `IAsyncPolicy<HttpResponse>` parameter.
> Please be aware that, later an `AddResilienceHandler()` will be introduced in the `Microsoft.Extensions.Http.Resilience` package which is the successor of the `Microsoft.Extensions.Http.Polly`.

### Reducing thrown exceptions

In case of Circuit Breaker when it is either in the `Open` or `Isolated` state new requests are rejected immediately.

That means the strategy will throw either a `BrokenCircuitException` or an `IsolatedCircuitException` respectively.

❌ DON'T

Use guard expression to call `Execute{Async}` only if the circuit is not broken:

<!-- snippet: circuit-breaker-anti-pattern-reduce-thrown-exceptions -->
```cs
var stateProvider = new CircuitBreakerStateProvider();
var circuitBreaker = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new()
{
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
BreakDuration = TimeSpan.FromSeconds(0.5),
StateProvider = stateProvider
})
.Build();

if (stateProvider.CircuitState
is not CircuitState.Open
and not CircuitState.Isolated)
{
var response = await circuitBreaker.ExecuteAsync(static async ct =>
{
return await IssueRequest();
}, CancellationToken.None);

// Your code goes here to process response
}
```
<!-- endSnippet -->

**Reasoning**:

- The problem with this approach is that the circuit breaker will never transition into the `HalfOpen` state.
- The circuit breaker does not act as an active object. In other words the state transition does not happen automatically in the background.
- The circuit transition into the `HalfOpen` state when the `Execute{Async}` method is called and the `BreakDuration` elapsed.

✅ DO

Use `ExecuteOutcomeAsync` to avoid throwing exception:

<!-- snippet: circuit-breaker-pattern-reduce-thrown-exceptions -->
```cs
var context = ResilienceContextPool.Shared.Get();
var circuitBreaker = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new()
{
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
BreakDuration = TimeSpan.FromSeconds(0.5),
})
.Build();

Outcome<HttpResponseMessage> outcome = await circuitBreaker.ExecuteOutcomeAsync(static async (ctx, state) =>
{
var response = await IssueRequest();
return Outcome.FromResult(response);
}, context, "state");

ResilienceContextPool.Shared.Return(context);

if (outcome.Exception is BrokenCircuitException)
{
// The execution was stopped by the circuit breaker
}
else
{
HttpResponseMessage response = outcome.Result!;
// Your code goes here to process the response
}
```
<!-- endSnippet -->

**Reasoning**:

- The `ExecuteOutcomeAsync` is a low-allocation API which does not throw exceptions; rather it captures them inside an `Outcome` data structure.
- Since you are calling one of the `Execute` methods, that's why the circuit breaker can transition into the `HalfOpen` state.
Loading
Loading