Preventing our Circuit Breaker / RETRY from tripping due to certain Exceptions.
The Resilience4j circuit breaker by default considers any exception thrown inside of the Supplier as a failure. If the failure rate exceeds the failure threshold percentage and the rolling window max size is met, then it will prevent any future calls from going through. There must be many application exceptions that the circuit breaker should ignore when determining whether to open or close the circuit. The circuit breaker is a design pattern used to improve the resilience of a system by preventing requests to a service or component that is experiencing issues or failures.
It’s not uncommon to throw exceptions as a part of normal business logic — they might be thrown because of an exceptional circumstance, but that doesn’t mean that there is an error or something wrong with the downstream resource you’re trying to interact with. For example, Spring’s Rest Template can throw an exception on a 4xx response code, and this will by default trip the circuit and prevent future calls from going through.
Note : Please check my previous article for the basics of circuit breaker and retry pattern in microservices.
Implementation
In this article we will be taking the same project we made in our last article where we have used Feign client for the network calls and invoking HTTP requests to external services. We will be ignoring all the exceptions between 4.x.x(Client error ) and 5.x.x(Server error) series. The first step is to handle every exception and verify them whether they can be ignored or not. We will be using the ErrorDecoder provided by the feign client library to interpret every error response before returning it to the main method. If the error response have a status code between 400(inclusive) and 500 we will throw an application exception which we will be ignored by the circuit breaker as well as by retry.
public class FeignClientDecoder implements ErrorDecoder {
Logger log = LoggerFactory.getLogger(FeignClientDecoder.class);
private final ErrorDecoder defaultErrorDecoder = new Default();
@Override
public Exception decode(String feignClientClassName, Response response) {
if (response.status() >= HttpStatus.BAD_REQUEST.value()
&& response.status() < HttpStatus.INTERNAL_SERVER_ERROR.value()) {
log.error(
"FeignClientErrorDecoder response received for feign client {} with response: {}",
feignClientClassName,
response);
return new ExternalServiceException("Bad Response from external services");
}
return defaultErrorDecoder.decode(feignClientClassName, response);
}
}
The above override method will be called in case of the exception thrown by the feign client. We can ignore the above application exception i.e ExternalServiceException through the ignoreExceptions property in the application.properties as shown below.
resilience4j.circuitbreaker.configs.default.failureRateThreshold=50
resilience4j.circuitbreaker.configs.default.ignoreExceptions=com.cb.microservice.exception.ExternalServiceException
resilience4j.circuitbreaker.instances.peopleProxyCircuitBreaker.baseConfig=default
resilience4j.retry.configs.default.maxAttempts=4
resilience4j.retry.configs.default.ignoreExceptions=com.cb.microservice.exception.ExternalServiceException
resilience4j.retry.instances.peopleProxyRetry.baseConfig=default
To test the above code, we will be exposing an API endpoint in the people service which will throw a Bad Request (400) exception and also change the feign client url in the main application i.e microservice as shown below.
@GetMapping("/exception")
public ResponseEntity<Void> getExceptionRespone() {
return ResponseEntity.badRequest().build();
}
@FeignClient(name = "people-api", url = "${http://localhost:8080/exception}")
public interface DataProxy {
Logger log = LoggerFactory.getLogger(DataProxy.class);
@GetMapping
@Retry(name = "peopleProxyRetry")
@CircuitBreaker(name = "peopleProxyCircuitBreaker", fallbackMethod = "serviceFallbackMethod")
ResponseEntity<List<People>> getCurrentUsers();
default ResponseEntity<List<People>> serviceFallbackMethod(Throwable exception) {
log.error(
"Data server is either unavailable or malfunctioned due to {}", exception.getMessage());
throw new RuntimeException(exception.getMessage());
}
}
By calling the main API which was http://localhost:8081/data , one can find that the retry and circuit breaker ignores the exception as they lie under the Client error series which we handled to get ignored. You can find that you will get only one callback exception log in this case as shown below.
Also if you will see the circuit breaker metrices, there will be no calls considered for the OPEN or CLOSED states. The circuit will remain in the CLOSED state with zero calls being permitted.
Note : You can download the above code from https://github.com/nitsbat/circuit-breaker/tree/CB-tripiing-from-Exceptions (Branch : CB-tripiing-from-Exceptions)
Conclusion
We successfully understand and implemented how to prevent tripping of our circuit by some application/business logic related exceptions.