diff --git a/docs/migration-v8.md b/docs/migration-v8.md index 0a6179f6dc9..78f8e9fed71 100644 --- a/docs/migration-v8.md +++ b/docs/migration-v8.md @@ -597,6 +597,178 @@ ResiliencePipeline 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: + + +```cs +// Create sync circuit breaker +ISyncPolicy syncPolicy = Policy + .Handle() + .CircuitBreaker( + exceptionsAllowedBeforeBreaking: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + +// Create async circuit breaker +IAsyncPolicy asyncPolicy = Policy + .Handle() + .CircuitBreakerAsync( + exceptionsAllowedBeforeBreaking: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + +// Create generic sync circuit breaker +ISyncPolicy syncPolicyT = Policy + .Handle() + .CircuitBreaker( + handledEventsAllowedBeforeBreaking: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + +// Create generic async circuit breaker +IAsyncPolicy asyncPolicyT = Policy + .Handle() + .CircuitBreakerAsync( + handledEventsAllowedBeforeBreaking: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); +``` + + +V7's Advanced Circuit Breaker policy could be defined like below: + + +```cs +// Create sync advanced circuit breaker +ISyncPolicy syncPolicy = Policy + .Handle() + .AdvancedCircuitBreaker( + failureThreshold: 0.5d, + samplingDuration: TimeSpan.FromSeconds(5), + minimumThroughput: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + +// Create async advanced circuit breaker +IAsyncPolicy asyncPolicy = Policy + .Handle() + .AdvancedCircuitBreakerAsync( + failureThreshold: 0.5d, + samplingDuration: TimeSpan.FromSeconds(5), + minimumThroughput: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + +// Create generic sync advanced circuit breaker +ISyncPolicy syncPolicyT = Policy + .Handle() + .AdvancedCircuitBreaker( + failureThreshold: 0.5d, + samplingDuration: TimeSpan.FromSeconds(5), + minimumThroughput: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + +// Create generic async advanced circuit breaker +IAsyncPolicy asyncPolicyT = Policy + .Handle() + .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 +``` + + +### 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. + + +```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(), + FailureRatio = 0.5d, + SamplingDuration = TimeSpan.FromSeconds(5), + MinimumThroughput = 2, + BreakDuration = TimeSpan.FromSeconds(1) + }) + .Build(); + +// Create a generic pipeline with circuit breaker. Because ResiliencePipeline supports both sync and async +// callbacks, there is also no need to define it twice. +ResiliencePipeline pipelineT = new ResiliencePipelineBuilder() + .AddCircuitBreaker(new CircuitBreakerStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(), + 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(), + 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 +``` + + +> [!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{}` 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: diff --git a/docs/strategies/circuit-breaker.md b/docs/strategies/circuit-breaker.md index e3bcfb9ad0e..fc003c89f1c 100644 --- a/docs/strategies/circuit-breaker.md +++ b/docs/strategies/circuit-breaker.md @@ -535,3 +535,85 @@ public Downstream1Client( > The above sample code used the `AsAsyncPolicy()` method to convert the `ResiliencePipeline` to `IAsyncPolicy`. > It is required because the `AddPolicyHandler()` method anticipates an `IAsyncPolicy` 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: + + +```cs +var stateProvider = new CircuitBreakerStateProvider(); +var circuitBreaker = new ResiliencePipelineBuilder() + .AddCircuitBreaker(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + 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 +} +``` + + +**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: + + +```cs +var context = ResilienceContextPool.Shared.Get(); +var circuitBreaker = new ResiliencePipelineBuilder() + .AddCircuitBreaker(new() + { + ShouldHandle = new PredicateBuilder().Handle(), + BreakDuration = TimeSpan.FromSeconds(0.5), + }) + .Build(); + +Outcome 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 +} +``` + + +**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. diff --git a/src/Snippets/Docs/CircuitBreaker.cs b/src/Snippets/Docs/CircuitBreaker.cs index b4990c9231c..a02b53c368f 100644 --- a/src/Snippets/Docs/CircuitBreaker.cs +++ b/src/Snippets/Docs/CircuitBreaker.cs @@ -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(), + 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 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(), + BreakDuration = TimeSpan.FromSeconds(0.5), + }) + .Build(); + + Outcome 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 + } } diff --git a/src/Snippets/Docs/Migration.CircuitBreaker.cs b/src/Snippets/Docs/Migration.CircuitBreaker.cs new file mode 100644 index 00000000000..7464fdb8c25 --- /dev/null +++ b/src/Snippets/Docs/Migration.CircuitBreaker.cs @@ -0,0 +1,152 @@ +using System.Net.Http; +using Polly.CircuitBreaker; +using Snippets.Docs.Utils; + +namespace Snippets.Docs; + +internal static partial class Migration +{ + public static void CircuitBreaker_V7() + { + #region migration-circuit-breaker-v7 + + // Create sync circuit breaker + ISyncPolicy syncPolicy = Policy + .Handle() + .CircuitBreaker( + exceptionsAllowedBeforeBreaking: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + + // Create async circuit breaker + IAsyncPolicy asyncPolicy = Policy + .Handle() + .CircuitBreakerAsync( + exceptionsAllowedBeforeBreaking: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + + // Create generic sync circuit breaker + ISyncPolicy syncPolicyT = Policy + .Handle() + .CircuitBreaker( + handledEventsAllowedBeforeBreaking: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + + // Create generic async circuit breaker + IAsyncPolicy asyncPolicyT = Policy + .Handle() + .CircuitBreakerAsync( + handledEventsAllowedBeforeBreaking: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + + #endregion + } + + public static void AdvancedCircuitBreaker_V7() + { + #region migration-advanced-circuit-breaker-v7 + + // Create sync advanced circuit breaker + ISyncPolicy syncPolicy = Policy + .Handle() + .AdvancedCircuitBreaker( + failureThreshold: 0.5d, + samplingDuration: TimeSpan.FromSeconds(5), + minimumThroughput: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + + // Create async advanced circuit breaker + IAsyncPolicy asyncPolicy = Policy + .Handle() + .AdvancedCircuitBreakerAsync( + failureThreshold: 0.5d, + samplingDuration: TimeSpan.FromSeconds(5), + minimumThroughput: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + + // Create generic sync advanced circuit breaker + ISyncPolicy syncPolicyT = Policy + .Handle() + .AdvancedCircuitBreaker( + failureThreshold: 0.5d, + samplingDuration: TimeSpan.FromSeconds(5), + minimumThroughput: 2, + durationOfBreak: TimeSpan.FromSeconds(1)); + + // Create generic async advanced circuit breaker + IAsyncPolicy asyncPolicyT = Policy + .Handle() + .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 + + #endregion + } + + public static async Task CircuitBreaker_V8() + { + #region migration-circuit-breaker-v8 + + // 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(), + FailureRatio = 0.5d, + SamplingDuration = TimeSpan.FromSeconds(5), + MinimumThroughput = 2, + BreakDuration = TimeSpan.FromSeconds(1) + }) + .Build(); + + // Create a generic pipeline with circuit breaker. Because ResiliencePipeline supports both sync and async + // callbacks, there is also no need to define it twice. + ResiliencePipeline pipelineT = new ResiliencePipelineBuilder() + .AddCircuitBreaker(new CircuitBreakerStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(), + 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(), + 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 + + #endregion + } +} diff --git a/src/Snippets/Docs/Migration.Retry.cs b/src/Snippets/Docs/Migration.Retry.cs index de97e046166..d073562a182 100644 --- a/src/Snippets/Docs/Migration.Retry.cs +++ b/src/Snippets/Docs/Migration.Retry.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Http; -using System.Runtime.Versioning; using Polly.Retry; using Snippets.Docs.Utils; @@ -8,7 +7,6 @@ namespace Snippets.Docs; internal static partial class Migration { - [RequiresPreviewFeatures] public static void Retry_V7() { #region migration-retry-v7