Building resilient .NET applications with Polly - Part 6
In this post, we will examine the concept of a circuit breaker, understand its utility, and explore how to implement it using Polly.
What is a circuit breaker ?
A circuit breaker is a design pattern used to enhance the resilience and reliability of systems by mitigating the impact of failures. It functions similarly to an electrical circuit breaker, which interrupts the flow of electricity when a fault is detected, preventing further damage. In the context of software, a circuit breaker monitors the interactions between components or services and can temporarily block access to a failing component or service when it detects repeated failures. Here is an outstanding article by Martin Fowler on this subject.
The basic idea behind the circuit breaker is very simple. You wrap a protected function call in a circuit breaker object, which monitors for failures. Once the failures reach a certain threshold, the circuit breaker trips, and all further calls to the circuit breaker return with an error, without the protected call being made at all.
Fowler
Hence, a circuit breaker differs from a retry in that its purpose is to prevent operations rather than reexecute them. The primary purpose of a circuit breaker is to prevent a system from repeatedly attempting to execute an operation that is likely to fail, either due to persistent issues or during transient faults. By introducing a circuit breaker, we can minimize the impact of failures, reduce resource consumption, and prevent the system from entering a state of prolonged degradation.
Simulating recurrent errors
- Edit the FaultyService class.
1 [FunctionName(nameof(GetReturnAlwaysFailures))]
2 public async Task<IActionResult> GetReturnAlwaysFailures([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
3 {
4 return new InternalServerErrorResult();
5 }
In this case, we are simulating recurring failures by deliberately initiating a bad request consistently.
- Edit the CallingService class in order to call this method.
1 [FunctionName(nameof(GetAccountById03))]
2 public async Task<IActionResult> GetAccountById03([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
3 {
4 var client = new HttpClient();
5 var response = await client.GetAsync("http://localhost:7271/api/GetReturnAlwaysFailures").ConfigureAwait(false);
6
7 return response.IsSuccessStatusCode ? new OkResult() : new InternalServerErrorResult();
8 }
Upon executing this request via Fiddler, we can indeed observe that an error systematically occurs.
Implementing a circuit breaker with Polly
Polly provides a convenient means to implement a circuit breaker effortlessly.
- Edit the CallingService class to implement a circuit breaker.
1[FunctionName(nameof(GetAccountById03))]
2public async Task<IActionResult> GetAccountById03([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
3{
4 var pipeline = CircuitBreakerSingleton.Instance.Pipeline;
5
6 try
7 {
8 // Execute the pipeline asynchronously
9 var response = await pipeline.ExecuteAsync(async token =>
10 {
11 var client = new HttpClient();
12 return await client.GetAsync("http://localhost:7271/api/GetReturnAlwaysFailures", token).ConfigureAwait(false);
13 }).ConfigureAwait(false);
14
15 return response.IsSuccessStatusCode ? new OkResult() : new InternalServerErrorResult();
16 }
17 catch (BrokenCircuitException bce)
18 {
19 // Execute specific action...
20 return new BadRequestObjectResult("The remote service is not available.");
21 }
22}
By definition, a circuit breaker spans multiple requests to verify that a remote service is effectively down. Therefore, it is essential in our case to create a singleton to ensure that there is only one instance of this circuit breaker, and we do not recreate it with each request.
1public class CircuitBreakerSingleton
2{
3 private static CircuitBreakerSingleton _instance;
4
5 private CircuitBreakerStateProvider _provider;
6 private ResiliencePipeline<HttpResponseMessage> _pipeline;
7
8 private CircuitBreakerSingleton()
9 {
10 _provider = new CircuitBreakerStateProvider();
11 var options = new CircuitBreakerStrategyOptions<HttpResponseMessage>
12 {
13 FailureRatio = 0.1,
14 SamplingDuration = TimeSpan.FromSeconds(30),
15 MinimumThroughput = 2,
16 BreakDuration = TimeSpan.FromSeconds(30),
17 ShouldHandle = new PredicateBuilder<HttpResponseMessage>().HandleResult(response => response.StatusCode == HttpStatusCode.InternalServerError),
18 StateProvider = _provider
19 };
20
21 _pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>().AddCircuitBreaker(options).Build();
22 }
23
24 public static CircuitBreakerSingleton Instance
25 {
26 get
27 {
28 if (_instance == null) _instance = new CircuitBreakerSingleton();
29 return _instance;
30 }
31 }
32
33 public ResiliencePipeline<HttpResponseMessage> Pipeline
34 {
35 get
36 {
37 return _pipeline;
38 }
39 }
40
41 public CircuitState State
42 {
43 get
44 {
45 return _provider.CircuitState;
46 }
47 }
48}
Once again, in real-world scenarios, the circuit breaker will be obtained through dependency injection.
Upon executing this request via Fiddler, we can now observe that these recurrent failures are effectively handled: the initial requests may fail, but for subsequent ones, the circuit breaker triggers and halts further requests to the flawed service. In our case, it returns an error 400, but, of course, it can be adapted to suit different situations.
Additional configuration options are available to customize the circuit breaker strategy. For more comprehensive details, please refer to the documentation.
In the concluding post, we will delve into the integration and combination of multiple strategies to be applied to a single request.