If we have a chain of services calling other services and one of the services is down or slow, it would impact the entire chain. In that case, we would need to return a fallback response. We could implement a circuit breaker pattern to reduce the load, retry the requests, and implement a rate limiting.
Resilience4j is a lightweight fault tolerance library designed for functional programming.
https://resilience4j.readme.io/docs
Dependencies
<dependency>
<artifactId>resilience4j-spring-boot2</artifactId>
<groupId>io.github.resilience4j</groupId>
</dependency>
<dependency>
<artifactId>spring-boot-starter-aop</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
<dependency>
<artifactId>spring-boot-starter-actuator</artifactId>
<groupId>org.springframework.boot</groupId>
</dependency>
Retry
When an API call fails, you can force a retry just by adding the @Retry annotation to the API endpoint. By default, it would retry three times, but you can set the max retry attempts by giving the retry a name and adding a max attempts property:
@GetMapping("/sample-api")
@Retry(name = "sample-api")
public String sampleApi() {
...
}
resilience4j.retry.instances.sample-api.max-attempts=5
Additionally, you can define other properties as wait duration (between attempts), and enable exponential backoff to wait in exponential ranges of time:
resilience4j.retry.instances.sample-api.waitDuration=1s
resilience4j.retry.instances.sample-api.enableExponentialBackoff=true
Fallback
You can define a fallback method in case after retries it fails again:
@GetMapping("/sample-api")
@Retry(name = "sample-api", fallbackMethod = "hardcodedResponse")
public String sampleApi() {
...
}
public String hardcodedResponse(Exception ex) {
return "fallback-response";
}
Circuit breaker
@GetMapping("/sample-api")
@CircuitBreaker(name = "default", fallbackMethod = "hardcodedResponse")
public String sampleApi() {
...
}
public String hardcodedResponse(Exception ex) {
return "fallback-response";
}
With this configuration, if sampleApi method fails, the fallback method is executed, for the next calls it will break the circuit and it will directly return the fallback response.
The CircuitBreaker is implemented via a finite state machine with three normal states: CLOSED, OPEN, and HALF_OPEN.
https://resilience4j.readme.io/docs/circuitbreaker
Closed is when I am calling the microservice continuously. In the open state, the circuit breaker will not call the dependent microservice. It will return the fallback response. Finally, in the HALF_OPEN state, the circuit breaker will be sending a percentage of requests to the dependent microservice, and for the rest of the requests, it will return the fallback response. When starting the application the circuit breaker starts in the closed state. If a significant percentage of the calls are failing, the circuit breaker will switch to the open state. There is a wait duration that you can configure, then it will switch to HALF_OPEN state. You can configure how much percentage of requests it will try to send successfully to the dependant service to move to the closed state again. If it fails, it will move back to the open state again.
Example of a configuration for failure rate threshold in percentage, the default value is 50%:
resilience4j.circuitbreaker.instances.default.failureRateThreshold=90
For more information see: https://www.baeldung.com/spring-boot-resilience4j
Rate Limiting
The @RateLimiter annotation allows us to define a period for a specific number of calls. If you exceed that quantity of calls, the service will return an exception.
@GetMapping("/sample-api")
@RateLimiter(name="default")
public String sampleApi() {
...
}
resilience4j.ratelimiter.instances.default.limitForPeriod=2
resilience4j.ratelimiter.instances.default.limitRefreshPeriod=10s
BulkHead
The @Bulkhead annotation allows us to define a max concurrent calls value.
@GetMapping("/sample-api")
@Bulkhead(name = "sample-api")
public String sampleApi() {
...
}
resilience4j.bulkhead.instances.sample-api.maxConcurrentCalls=10
Top comments (0)