Reputation: 89
So, I was doing a simple spring boot project using resilience4j's tools, like @RateLimiter
, @CircuitBreaker
and @Retry
. All was working fine, even with personalized exceptions, because they were caught within my service.
Now I want my service to simply throw exceptions, and manage the handling in a @RestControllerAdvice
with @ExceptionHandler
(s), instead of catching the exceptions and managing them within the service, as I see many people do it like that. It also makes sense, because that way, the service only does strictly what it needs to do, and another class can manage the exceptions.
The problem is that, from what I've seen (not much), all these people that do that don't use @CircuitBreaker
too. To be more specific, the problem is that the exceptions are caught first by the Circuit Breaker's fallback method instead of the Exception Handlers.
Here's some code:
@CircuitBreaker(name = "clientCircuitBreaker", fallbackMethod = "fallbackCircuitBreaker")
@Retry(name="clientRetry")
@RateLimiter(name="clientRateLimiter", fallbackMethod = "fallbackRateLimiter")
@GetMapping
public ResponseEntity<?> getClient(@RequestParam long doc) {
return this.clientService.getClient(doc);
}
public ResponseEntity<Object> fallbackCircuitBreaker(Throwable exception) {
return this.clientService.fallbackCircuitBreaker(exception);
}
//...
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IErrorException.class)
public ResponseEntity<ErrorModel> personalizedException(IErrorException ex) {
return ResponseEntity.status(404).body(ex.getErrorModel());
}
//...
}
I've also tried changing the type of the @ExceptionHandler
to a more specific one (and make it throw that exception), because that right there is an interface/abstract class, but nothing. I also tried using the configuration ignoreExceptions(..)
for the Circuit Breaker in a java configuration file, ignoring 'IErrorException.class', but nothing.
Any idea? Or should I give up this line of thought, and just manage exceptions inside of the service?
Upvotes: 1
Views: 1851
Reputation: 89
Update: I've found a better solution.
So, it's been a while. I've been running with the above answer, but I've come to understand that it's a bit lacking. The reason is that, if you now need to ignore some new exceptions, that aren't a subclass of IErrorException
(example: internal database related exceptions), you have to add a new method for each one, which is trash.
So I was thinking about how this normally works (when I don't add any fallback method and I configure the exceptions to be ignored in the application.yml/properties
). In that case you just add a list of exceptions you want to ignore and that's that. So my new solution follows that idea, though to be honest I still feel like it could be improved.
@RequestMapping("/api/${env.version}/clients")
@RestController
public class ClientController implements IClientController {
private final ClientService clientService;
private final CircuitBreakerRegistry circuitBreakerRegistry;
@Autowired
public ClientController(ClientService clientService, CircuitBreakerRegistry circuitBreakerRegistry) {
this.clientService = clientService;
this.circuitBreakerRegistry = circuitBreakerRegistry;
}
@CircuitBreaker(name = "circuitBreaker", fallbackMethod = "fallbackCircuitBreaker")
@Retry(name = "retry")
@RateLimiter(name = "rateLimiter", fallbackMethod = "fallbackRateLimiter")
@GetMapping
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public ClientModel getClient(@RequestParam long doc) {
return clientService.getClient(doc);
}
// fallbacks
public ResponseModel fallbackCircuitBreaker(long doc, Exception exception) throws Exception {
ArrayList<Class> ignoredExceptions = new ArrayList<>();
ignoredExceptions.add(DataAccessException.class);
ignoredExceptions.add(IErrorException.class);
ignoredExceptions.add(RateLimiterException.class);
if (ignoredExceptions.stream().anyMatch(ex -> ex.isAssignableFrom(exception.getClass()))) {
circuitBreakerRegistry.circuitBreaker("circuitBreaker").transitionToClosedState();
throw exception;
}
throw new CircuitBreakerException();
}
public ClientModel fallbackRateLimiter(long doc, RequestNotPermitted exception) {
throw new RateLimiterException();
}
}
Thus, if you now need to ignore a new exception, you just copy the last ignoredExceptions.add(...)
line and just change the exception to the one you want. Just like that it works.
Like I also said, I think there's still room for improvement here, like maybe using the properties from appliaction.yml/properties
that are normally used to ignore exceptions for the circuit breaker (that don't work here, because I'm setting fallbaks) by catching them with @Value
as a list inside the controller or something, thus never even needing to change the code again, just the appliaction.yml/properties
. But we'll see.
For just a few exceptions, the previous answer was good enough, but for more (3 or more in my opinion), I think this is better.
Upvotes: 0
Reputation: 89
Update: this is my solution for now
@RequestMapping("/api/${env.version}/clients")
@RestController
public class ClientController implements IClientController {
private final ClientService clientService;
private final CircuitBreakerRegistry circuitBreakerRegistry;
@Autowired
public ClientController(ClientService clientService, CircuitBreakerRegistry circuitBreakerRegistry) {
this.clientService = clientService;
this.circuitBreakerRegistry = circuitBreakerRegistry;
}
@CircuitBreaker(name = "clientCircuitBreaker", fallbackMethod = "fallbackCircuitBreaker")
@Retry(name = "clientRetry")
@RateLimiter(name = "clientRateLimiter", fallbackMethod = "fallbackRateLimiter")
@GetMapping
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public ClientModel getClient(@RequestParam long doc) {
return clientService.getClient(doc);
}
// fallbacks
public ClientModel fallbackCircuitBreaker(long doc, IErrorException exception) {
circuitBreakerRegistry.circuitBreaker("clientCircuitBreaker").transitionToClosedState();
throw exception;
}
public ClientModel fallbackCircuitBreaker(long doc, Throwable exception) {
throw new CircuitBreakerException();
}
public ClientModel fallbackRateLimiter(long doc, RequestNotPermitted exception) {
throw new RateLimiterException();
}
}
This is the best I can get without being able to use ignoreExceptions
. About that, I confirmed that ignoreExceptions
for Retry
works, but ignoreExceptions
for Circuit Breaker
doesn't.
I thought that the fact that I'm using a fallback method for Circuit Breaker
and not for Retry
could have something to do with it (maybe ???). This solution and what I just said come from here. Although, it's not ideal, because what I'm doing when catching the IErrorException exception
is just throwing it again and just ignoring the intervention of the Circuit Breaker
.
Upvotes: 0
Reputation: 89
Quick note: the annotations are executed in the inverse order that they appear, meaning @RateLimiter
, then @Retry
, then @CircuitBreaker
. The circuit breaker, therefore, is the last failsafe to catch any unexpected exceptions.
Hi, I'm the one that asked the question, and I have found a temporary (and rather sus in my opinion) solution.
It goes like this:
public ResponseEntity<Object> fallbackCircuitBreaker(Throwable exception) throws Throwable {
if(!(exception instanceof IErrorException)) {
return this.clienteService.fallbackCircuitBreaker(exception);
} else {
circuitBreakerRegistry.circuitBreaker("clientCircuitBreaker").transitionToClosedState();
throw exception;
}
}
Like I said, it solves it because, even though the circuit breaker still catches it, if it's a personalized exception, it re-throws it, and it's then caught by the exception handlers. The problem is that it does still trigger the circuit breaker, so, if it was a personalized exception, I also force a transition to closed state to "make it so nothing happened". Suspicious, as I said before.
Something of note is that I also have a @Retry
like it shows in my controller, so the above solution worked for the circuit breaker, but it still retried a couple of times when throwing a personalized exception. To solve this, I ventured to use .ignoreExceptions(IErrorException.class)
in the retry configuration, which didn't work in the circuit breaker configuration last time I chequed. But surprisingly, it worked.
Thus, I reached the intended behaviour. It works just like catching my personalized exceptions in the service and returning a proper reponse, just that it's better distributed now. However, I feel like it's not the best option. So I ask: does something like .ignoreExceptions(IErrorException.class)
work for you in your circuit breaker configuration?
Here are my dependencies and configs (relevant to this):
@Configuration
public class CircuitBreakerConfiguration {
@Primary
@Bean
public CircuitBreakerProperties circuitBreakerProperties() {
CircuitBreakerProperties circuitBreakerProperties = new CircuitBreakerProperties();
circuitBreakerProperties.setCircuitBreakerAspectOrder(1);
return circuitBreakerProperties;
}
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(5)
.failureRateThreshold(40)
.slowCallRateThreshold(40)
.permittedNumberOfCallsInHalfOpenState(1)
.maxWaitDurationInHalfOpenState(Duration.ofSeconds(10))
.waitDurationInOpenState(Duration.ofSeconds(10))
.writableStackTraceEnabled(true)
.minimumNumberOfCalls(5)
.automaticTransitionFromOpenToHalfOpenEnabled(true)
.ignoreExceptions(IErrorException.class) // last time I chequed, it did nothing
.build();
return CircuitBreakerRegistry.of(circuitBreakerConfig);
}
}
@Configuration
public class RetryConfiguration {
@Primary
@Bean
public RetryProperties retryProperties() {
RetryProperties retryProperties = new RetryProperties();
retryProperties.setRetryAspectOrder(2);
return retryProperties;
}
@Bean
public RetryRegistry retryRegistry() {
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofSeconds(1))
.ignoreExceptions(IErrorException.class) // works
.build();
return RetryRegistry.of(retryConfig);
}
}
<!-- ... -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- ... -->
<dependencies>
<!--Resilience4j (Circuit Breaker, Rate Limiter y Retry)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.1.0</version>
</dependency>
<!-- ... -->
<dependencies>
<!-- ... -->
Upvotes: 1