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

[Docs] Add circuit breaker to the migration guide #1764

172 changes: 172 additions & 0 deletions docs/migration-v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,178 @@ ResiliencePipeline<HttpResponseMessage> pipelineT = new ResiliencePipelineBuilde
>
> For further information please check out the [Timeout resilience strategy documentation](strategies/timeout.md).

## Migrating circuit breaker policies

This section describes how to migrate v7 circuit breaker policies to V8 circuit breaker strategies.

### Circuit breaker in v7

V7's "Standard" Circuit Breaker policy could be defined like below:

<!-- snippet: migration-circuit-breaker-v7 -->
```cs
// Create sync circuit breaker
ISyncPolicy syncPolicy = Policy
.Handle<SomeExceptionType>()
.CircuitBreaker(
exceptionsAllowedBeforeBreaking: 2,
durationOfBreak: TimeSpan.FromSeconds(1));

// Create async circuit breaker
IAsyncPolicy asyncPolicy = Policy
.Handle<SomeExceptionType>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 2,
durationOfBreak: TimeSpan.FromSeconds(1));

// Create generic sync circuit breaker
ISyncPolicy<HttpResponseMessage> syncPolicyT = Policy<HttpResponseMessage>
.Handle<SomeExceptionType>()
.CircuitBreaker(
handledEventsAllowedBeforeBreaking: 2,
durationOfBreak: TimeSpan.FromSeconds(1));

// Create generic async circuit breaker
IAsyncPolicy<HttpResponseMessage> asyncPolicyT = Policy<HttpResponseMessage>
.Handle<SomeExceptionType>()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 2,
durationOfBreak: TimeSpan.FromSeconds(1));
```
<!-- endSnippet -->

V7's Advanced Circuit Breaker policy could be defined like below:

<!-- snippet: migration-advanced-circuit-breaker-v7 -->
```cs
// Create sync advanced circuit breaker
ISyncPolicy syncPolicy = Policy
.Handle<SomeExceptionType>()
.AdvancedCircuitBreaker(
failureThreshold: 0.5d,
samplingDuration: TimeSpan.FromSeconds(5),
minimumThroughput: 2,
durationOfBreak: TimeSpan.FromSeconds(1));

// Create async advanced circuit breaker
IAsyncPolicy asyncPolicy = Policy
.Handle<SomeExceptionType>()
.AdvancedCircuitBreakerAsync(
failureThreshold: 0.5d,
samplingDuration: TimeSpan.FromSeconds(5),
minimumThroughput: 2,
durationOfBreak: TimeSpan.FromSeconds(1));

// Create generic sync advanced circuit breaker
ISyncPolicy<HttpResponseMessage> syncPolicyT = Policy<HttpResponseMessage>
.Handle<SomeExceptionType>()
.AdvancedCircuitBreaker(
failureThreshold: 0.5d,
samplingDuration: TimeSpan.FromSeconds(5),
minimumThroughput: 2,
durationOfBreak: TimeSpan.FromSeconds(1));

// Create generic async advanced circuit breaker
IAsyncPolicy<HttpResponseMessage> asyncPolicyT = Policy<HttpResponseMessage>
.Handle<SomeExceptionType>()
.AdvancedCircuitBreakerAsync(
failureThreshold: 0.5d,
samplingDuration: TimeSpan.FromSeconds(5),
minimumThroughput: 2,
durationOfBreak: TimeSpan.FromSeconds(1));

// Check circuit state
ICircuitBreakerPolicy cbPolicy = (ICircuitBreakerPolicy)asyncPolicy;
bool isOpen = cbPolicy.CircuitState == CircuitState.Open || cbPolicy.CircuitState == CircuitState.Isolated;

// Manually control state
cbPolicy.Isolate(); // Transitions into the Isolated state
cbPolicy.Reset(); // Transitions into the Closed state
```
<!-- endSnippet -->

### Circuit breaker in v8

> [!IMPORTANT]
>
> Polly V8 does not support the standard (*"classic"*) circuit breaker with consecutive failure counting.
>
> In case of V8 you can define a Circuit Breaker strategy which works like the advanced circuit breaker in V7.

<!-- snippet: migration-circuit-breaker-v8 -->
```cs
// Create pipeline with circuit breaker. Because ResiliencePipeline supports both sync and async
// callbacks, there is no need to define it twice.
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(),
FailureRatio = 0.5d,
SamplingDuration = TimeSpan.FromSeconds(5),
MinimumThroughput = 2,
BreakDuration = TimeSpan.FromSeconds(1)
})
.Build();

// Create a generic pipeline with circuit breaker. Because ResiliencePipeline<T> supports both sync and async
// callbacks, there is also no need to define it twice.
ResiliencePipeline<HttpResponseMessage> pipelineT = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
{
ShouldHandle = new PredicateBuilder<HttpResponseMessage>().Handle<SomeExceptionType>(),
FailureRatio = 0.5d,
SamplingDuration = TimeSpan.FromSeconds(5),
MinimumThroughput = 2,
BreakDuration = TimeSpan.FromSeconds(1)
})
.Build();

// Check circuit state
CircuitBreakerStateProvider stateProvider = new();
// Manually control state
CircuitBreakerManualControl manualControl = new();

ResiliencePipeline pipelineState = new ResiliencePipelineBuilder()
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<SomeExceptionType>(),
FailureRatio = 0.5d,
SamplingDuration = TimeSpan.FromSeconds(5),
MinimumThroughput = 2,
BreakDuration = TimeSpan.FromSeconds(1),
StateProvider = stateProvider,
ManualControl = manualControl
})
.Build();

// Check circuit state
bool isOpen = stateProvider.CircuitState == CircuitState.Open || stateProvider.CircuitState == CircuitState.Isolated;

// Manually control state
await manualControl.IsolateAsync(); // Transitions into the Isolated state
await manualControl.CloseAsync(); // Transitions into the Closed state
```
<!-- endSnippet -->

> [!NOTE]
>
> In case of V7 you could do an optimization to reduce the thrown exceptions.
>
> You could guard the `Execute{Async}` call with a condition that the circuit is not broken. This technique does **not** work with V8.
>
> Under the [circuit breaker's anti-patterns](strategies/circuit-breaker.md#4---reducing-thrown-exceptions) you can find the suggested way for V8.

____

> [!IMPORTANT]
>
> Things to remember:
>
> - Use `AddCircuitBreaker` to add a circuit breaker strategy to your resiliency pipeline
> - Use the `CircuitBreakerStrategyOptions{<TResult>}` to customize your circuit breaker behavior to meet your requirements
>
> For further information please check out the [Circuit Breaker resilience strategy documentation](strategies/circuit-breaker.md).

## Migrating other policies

Migrating is a process similar to the ones described in the previous sections. Keep in mind that:
Expand Down
82 changes: 82 additions & 0 deletions docs/strategies/circuit-breaker.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,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`.

### 4 - 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-4 -->
```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-4 -->
```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.
65 changes: 65 additions & 0 deletions src/Snippets/Docs/CircuitBreaker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,69 @@ [new Uri("https://downstreamN.com")] = GetCircuitBreaker()
await uriToCbMappings[downstream1Uri].ExecuteAsync(CallXYZOnDownstream1, CancellationToken.None);
#endregion
}

public static async ValueTask AntiPattern_4()
{
#region circuit-breaker-anti-pattern-4

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
}

#endregion
}

private static ValueTask<HttpResponseMessage> IssueRequest() => ValueTask.FromResult(new HttpResponseMessage());

public static async ValueTask Pattern_4()
{
#region circuit-breaker-pattern-4

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
}

#endregion
}
}
Loading