Circuit Breakers | Spring Cloud
In this article, we will learn about the Circuit Breaker Pattern and its implementation in Spring Boot using the Resilience4J Spring Cloud CircuitBreaker implementation.
Why Circuit Breakers?
Distributed systems depend on seamless inter-service communication, but disruptions can occur due to service outages, connectivity problems, and other factors. When dealing with services that rely on synchronous communication, extended delays or outages in the downstream service can lead to resource depletion and increased latency. This is precisely where the circuit breaker pattern comes into the picture.
With the Circuit breaker pattern, we can prevent the application from repeatedly retrying operations which are more likely to fail and enable this operation once the downstream service is healthy. This is achieved by maintaining a state machine consisting of the following three states —
- Closed Circuit: This is the ideal scenario, all calls to the downstream service work without any issues, also known as the “Happy Flow” in the diagram above. However, if the number of errors within a specified time window exceeds a certain threshold, the state transitions to “Open”.
- Open Circuit: When in the “Open” state, all calls to the downstream service immediately fail with an Exception, without any actual communication with the downstream service. A countdown timer is initiated whenever the circuit moves to the “Open” state to enable a transition to the “Half Open” state, where normal operation can resume.
- Half Open Circuit: During the “Half Open” state, a limited number of calls to the downstream service are permitted. If these calls are successful, the state returns to “Closed,” and normal operation resumes. If not, the state reverts to “Open” and the timer is reset.
Spring Integration
Adding Spring Dependencies
Add Resilience4j Circuit breaker Starter dependencies in “build.gradle” file.
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
Configuring Default Circuit Breaker Config
We start by creating a “CircuitBreaker” bean while configuring some of the basic settings for the circuit breaker:
- Window Size: Minimum number of calls which are required (per sliding window period) before the CircuitBreaker can calculate the error rate.
- Wait Duration: Fixed wait duration which controls how long the CircuitBreaker should stay open before it switches to half-open.
- Timeout Duration: The thread execution timeout for the downstream Service
@Configuration
public class Resilience4jCircuitBreakerConfig {
@Bean
Resilience4JCircuitBreaker circuitBreaker(Resilience4JCircuitBreakerFactory cbFactory) {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom(
.minimumNumberOfCalls(3)
.waitDurationInOpenState(Duration.ofSeconds(10))
.enableAutomaticTransitionFromOpenToHalfOpen()
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.cancelRunningFuture(true)
.timeoutDuration(Duration.ofMillis(500))
.build();
cbFactory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build());
return (Resilience4JCircuitBreaker) cbFactory.create("default1");
}
}
Integrating Circuit Breaker
To achieve this, we will utilize the run
method. The Supplier
the parameter represents the code you intend to wrap within the circuit breaker, while the Function
parameter represents the fallback that will be executed if the circuit breaker is triggered.
<T> T run(Supplier<T> toRun, Function<Throwable, T> fallback);
CircuitBreaker circuitBreaker;
@GetMapping
ResponseEntity<?> getData(@RequestParam(defaultValue = "fast") String type) {
Map<String, Object> response = new HashMap<>();
circuitBreaker.run(
() -> {
try {
HttpClient httpClient = HttpClient.newHttpClient();
URI uri = URI.create("http://localhost:8081/api/" + type);
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.build();
HttpResponse<String> value = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
response.put("Value", value.body());
return ResponseEntity.ok(response);
} catch (Exception ex) {
log.error(ex.toString());
response.put("Value", null);
response.put("Error", ex.toString());
return ResponseEntity.internalServerError().body(response);
}
},
// Fallback Method (In case, circuit is open)
ex -> {
log.error(ex.toString());
response.put("Value", null);
response.put("Error", ex.toString());
return ResponseEntity.badRequest().body(response);
}
)
}
Circuit Breaker Testing
We simulate the testing by creating two separate services, referred to as Service A and Service B.
- Service A: This service runs on port 8080 and is responsible for retrieving data from Service B and returning the response to the client.
- Service B: This service operates on port 8081 and provides two APIs for simulating responses — fast(< 100 ms) and slow responses(> 600 ms).
- Closed Circuit: Initially, we begin testing with the fast API, and we observe that responses are returned successfully.
- Closed Circuit with Slow API —We trigger the slow API on Service B, which has a response time of around 600 ms. This exceeds the circuit breaker timeout, leading to an exception.
- Open Circuit — After a sufficient number of slow requests fail, the circuit transitions to an “open” state. Despite the API call failing, the latency is significantly improved as the calls to Service B are skipped.
- Half Open Circuit — Once the waiting duration has passed, the circuit closes, and traffic is routed to the downstream services again. Now based on the subsequent requests, the circuit is moved to a “Closed” or “Open” state.
Feel free to check the code for more details on Github.