3

I am facing a issue with the circuit breaker implementation using Spring Cloud Resilience4j.

Following some tutorial, I have tried to add the necessary dependencies in the project. Also, tried to add the configurations but, still the circuit is not opening and fallback method is not getting called.

For the use case, I am calling an external API from my service and if that external API is down then after few calls I need to enable the circuit breaker.

Please find the code pieces from the different files.

I am a newbie to circuit breaker pattern. Any help will be highly appreciated.

pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>2.5.5</version>
   <relativePath/> <!-- lookup parent from repository -->
</parent>
    <properties>
    <java.version>11</java.version>
    <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
<dependencies>
</project>

Application properties

resilience4j.circuitbreaker.instances.test-api.register-health-indicator=true
resilience4j.circuitbreaker.instances.test-api.minimum-number-of-calls=4
resilience4j.circuitbreaker.instances.test-api.failure-rate-threshold=50
resilience4j.circuitbreaker.instances.test-api.permitted-number-of-calls-in-half-open-state=3
resilience4j.circuitbreaker.instances.test-api.wait-duration-in-open-state=30s
resilience4j.circuitbreaker.instances.test-api.automatic-transition-from-open-to-half-open-enabled=true
resilience4j.circuitbreaker.instances.test-api.record-exceptions=com.testapi.exception.ServiceUnavailableError

Service Class Code Piece

@CircuitBreaker(name = "test-api", fallbackMethod = "storeResponseFallback")
    public TestResponse storeResponse(String apiURL, HttpEntity<String> entityrequest) {

        TestResponse testResponse = new TestResponse();
        Optional<ResponseEntity<TestResponse>> response = Optional.empty();
        Future<ResponseEntity<TestResponse>> responseFuture;

        ExecutorService executor = Executors.newFixedThreadPool(10);

        log.debug("Calling Extrenal API, Request Body: {}", entityrequest.toString());

        try {
            //Service call returns a future
            responseFuture = executor.submit(() -> restTemplate.postForEntity(apiURL, entityrequest, TestResponse.class));
            response = Optional.ofNullable(responseFuture.get());
            log.info("Got response from external API");


            if ((response.isPresent()) && (response.get().hasBody())) {
                testResponse = response.get().getBody();
            }

        } catch (Exception exception) {
            log.error("External api call got failed with an error");
            Thread.currentThread().interrupt();            
            throw new ServiceUnavailableError();
        }


        return testResponse;

    }
    
    
    public TestResponse storeResponseFallback(ServiceUnavailableError ex) {
        log.error("Executing Fallback Method For General exceptions");
        throw new ServiceUnavailableError();
    }

ServiceUnavailableError Java file

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServiceUnavailableError extends RuntimeException{
    private static final long serialVersionUID = 2382122402994502766L;

    private String message;

}
sawsb123
  • 105
  • 2
  • 9
  • don't use try catch with circuit breaker, the circuit breaker is in itself a try catch. Make use of try.of to execute the supplier and the second parameter you provide will be your fallback method. You can go through the [link] https://www.youtube.com/watch?v=8yJ0xek6l6Y&t=120s – MonishGhutke Nov 09 '22 at 04:22
  • Also this is a annotation based approach, try doing functional approach where we create a circuitbreakerfactory bean and inject it in service class and make use of Try monad to execute the REST call. – MonishGhutke Nov 09 '22 at 04:27

2 Answers2

1

The signature of your fallback method is wrong. It should contain all the parameters of the actual method ( in your case storeResponseFallback is the fallback method and storeResponse is the actual method), along with the exception. Please make sure to remove the try catch block. You do not want to handle the exception yourself, rather you should let circuit breaker to handle it for you. Please take a look at the following code which is from given link https://resilience4j.readme.io/docs/getting-started-3

@CircuitBreaker(name = BACKEND, fallbackMethod = "fallback")
public Mono<String> method(String param1) {
    return Mono.error(new NumberFormatException());
}

private Mono<String> fallback(String param1, IllegalArgumentException e) {
    return Mono.just("test");
}

Try using the following yaml file I used the following configuration with your existing code,I used yaml instead of properties file. this seems to stay in open state and call only the fallback method.

resilience4j.circuitbreaker:
  configs:
    default:
      slidingWindowSize: 4
      permittedNumberOfCallsInHalfOpenState: 10
      waitDurationInOpenState: 10000
      failureRateThreshold: 60
      eventConsumerBufferSize: 10
      registerHealthIndicator: true
    someShared:
      slidingWindowSize: 3
      permittedNumberOfCallsInHalfOpenState: 10
  instances:
    test-api:
      baseConfig: default
      waitDurationInOpenState: 500000
    backendB:
      baseConfig: someShared

Here is the updated fallback method

public TestResponse storeResponseFallback(String apiURL, String entityrequest, java.lang.Throwable t) {
        log.error("Executing Fallback Method For General exceptions "+t.getMessage());
        return new TestResponse("Frm Fallback");// Making sure to send a blank response
    }
  • I have updated the method signature of the fallback method. But, still facing the same issue. `public TestResponse storeResponseFallback(String apiURL, HttpEntity entityrequest, ServiceUnavailableError ex) { log.error("Executing Fallback Method For General exceptions"); throw new ServiceUnavailableError(); }` – sawsb123 Nov 29 '21 at 05:11
  • What issue exactly you are getting? is it going to the catch block? – knowledge-seeker Nov 29 '21 at 05:13
  • Yes it is going to the catch block. In the log I can see that it is throwing the ServiceUnavailableError exception. In the postman call it is returning the expected error message also. But, after the defined number of the calls also it is not opening the circuit breaker. Instead, it is calling the main service method every time. – sawsb123 Nov 29 '21 at 05:16
  • Please remove the try catch block and then execute – knowledge-seeker Nov 29 '21 at 05:16
  • I have tried it. But still facing the same issue. – sawsb123 Nov 29 '21 at 05:26
  • What error you see after removing the try catch block? is it not printing "Executing Fallback Method For General exceptions"? – knowledge-seeker Nov 29 '21 at 05:28
  • No it is still executing the main method only. Not the fallback method. – sawsb123 Nov 29 '21 at 05:30
  • 1. When application running at apiURl is down, is it not printing the fallback message? is not then what is getting executed? 2. Are you hitting the application within 30 secs as you have mentioned in wait-duration-in-open-state? Can you increase the time to say 5 mins and try? Also, you have to make 4 failed calls to get your circuit at open state as you have mentioned at minimum-number-of-calls. – knowledge-seeker Nov 29 '21 at 05:33
  • When the apiURI is down and we have made minimum 4 calls to that api then circuit breaker should be open. But, it is not happening. Instead of opening circuit breaker it is still calling the actual method and allowing the calls. I have to increase the wait-duration-in-open-state property to 5 minutes. But, still circuit breaker is not getting triggered. – sawsb123 Nov 29 '21 at 05:40
  • with your existing code,I used yaml instead of properties file. resilience4j.circuitbreaker: configs: default: slidingWindowSize: 4 permittedNumberOfCallsInHalfOpenState: 10 waitDurationInOpenState: 10000 failureRateThreshold: 60 eventConsumerBufferSize: 10 registerHealthIndicator: true someShared: slidingWindowSize: 3 permittedNumberOfCallsInHalfOpenState: 10 instances: test-api: baseConfig: default waitDurationInOpenState: 500000 backendB: baseConfig: someShared – knowledge-seeker Nov 29 '21 at 06:58
  • Okay let me check again. As per my understanding, YAML or normal application.properties file should not make any difference. But, let me try it once. – sawsb123 Nov 29 '21 at 07:52
0

I have prepared the video, where I have defined main service and target service and I am preparing the bean from config and making use of Try.of() please check the video if it help. you will see that the fallback method is working fine. https://www.youtube.com/watch?v=8yJ0xek6l6Y&t=31s

MonishGhutke
  • 109
  • 1
  • 6