3

I've checked the questions on the site about Resilience4J, but have not had any luck with their answers. I'm trying to implement @CircuitBreaker annotation from Resilience4J in my Spring Boot 2.x project. The circuit breaker is implemented around a pretty straightforward function. However, when I supply a bad URL, the circuit is not opening, no matter how many times I send the request. I've gone so far as to extract everything into a standalone application and run it 100 times and observe it just endlessly failing. Any idea what I'm doing wrong?

    @CircuitBreaker(name = "backendA")
    @Component
    public class ResilientClient {

        private HttpClient httpClient;

        private static final Logger log = LoggerFactory.getLogger(ResilientClient.class);

        public ResilientClient() {

            httpClient = HttpClient.newBuilder().build();
        }


        @Bulkhead(name = "backendA")
        public String processPostRequest(String body, String[] headers, String url) {

            HttpResponse<String> response = null;

            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .headers(headers)
                .build();

            try {
                response =  httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            } catch (IOException e) {
                throw new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "This is a remote exception");
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.error("Interrupted Exception: " + e.getLocalizedMessage(), e);

            }
            return response != null ? response.body() : null;
    };

// None of these functions ever get invoked

    private String fallback(Throwable e){
        log.info("generic throwable caught");
        return "generic result";
    }

    private String fallback(String param1, String[] headers, String url, Throwable e) {
        log.info("Fallback method invoked for Throwable: " + param1);
        return null;
    }

    private String fallback(String param1, String[] headers, String url, ConnectException e) {
        log.info("Fallback method invoked for ConnectException: " + param1);
        return null;
    }

}


The config file is taken directly from the Github example

resilience4j.circuitbreaker:
  configs:
    default:
      registerHealthIndicator: false
      slidingWindowSize: 10
      minimumNumberOfCalls: 5
      permittedNumberOfCallsInHalfOpenState: 3
      automaticTransitionFromOpenToHalfOpenEnabled: true
      waitDurationInOpenState: 2s
      failureRateThreshold: 50
      eventConsumerBufferSize: 10
      recordExceptions:
        - org.springframework.web.client.HttpServerErrorException
        - java.io.IOException
      ignoreExceptions:
        - io.github.robwin.exception.BusinessException
    shared:
      registerHealthIndicator: true
      slidingWindowSize: 100
      permittedNumberOfCallsInHalfOpenState: 30
      waitDurationInOpenState: 1s
      failureRateThreshold: 50
      eventConsumerBufferSize: 10
      ignoreExceptions:
        - io.github.robwin.exception.BusinessException
  instances:
    backendA:
      baseConfig: default
    backendB:
      registerHealthIndicator: true
      slidingWindowSize: 10
      minimumNumberOfCalls: 10
      permittedNumberOfCallsInHalfOpenState: 3
      waitDurationInOpenState: 1s
      failureRateThreshold: 50
      eventConsumerBufferSize: 10
      recordFailurePredicate: io.github.robwin.exception.RecordFailurePredicate
resilience4j.retry:
  configs:
    default:
      maxRetryAttempts: 2
      waitDuration: 100
      retryExceptions:
        - org.springframework.web.client.HttpServerErrorException
        - java.io.IOException
      ignoreExceptions:
        - io.github.robwin.exception.BusinessException
  instances:
    backendA:
      maxRetryAttempts: 3
    backendB:
      maxRetryAttempts: 3
resilience4j.bulkhead:
  configs:
    default:
      maxConcurrentCalls: 100
  instances:
    backendA:
      maxConcurrentCalls: 10
    backendB:
      maxWaitDuration: 10ms
      maxConcurrentCalls: 20

resilience4j.thread-pool-bulkhead:
  configs:
    default:
      maxThreadPoolSize: 4
      coreThreadPoolSize: 2
      queueCapacity: 2
  instances:
    backendA:
      baseConfig: default
    backendB:
      maxThreadPoolSize: 1
      coreThreadPoolSize: 1
      queueCapacity: 1

resilience4j.ratelimiter:
  configs:
    default:
      registerHealthIndicator: false
      limitForPeriod: 10
      limitRefreshPeriod: 1s
      timeoutDuration: 0
      eventConsumerBufferSize: 100
  instances:
    backendA:
      baseConfig: default
    backendB:
      limitForPeriod: 6
      limitRefreshPeriod: 500ms
      timeoutDuration: 3s

Code to try testing it

SpringBootApplication
public class CircuitsApplication {

    private static final Logger logger = LoggerFactory.getLogger(CircuitsApplication.class);

    static ResilientClient resilientClient = new ResilientClient();

    public static void main(String[] args) {
        //SpringApplication.run(CircuitsApplication.class, args);
        for (int i = 0; i < 100; i++){
            try {
                String body = "body content";
                String[] headers = new String[]{"header", "value"};
                String url = "http://a.bad.url";
                String result = resilientClient.processPostRequest(body, headers, url);
                logger.info(result);
            } catch (Exception ex){
                logger.info("Error caught in main loop");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

I've tried adding the Circuitbreaker annotation to the method itself. I've tried creating a supplier and decorating it. I've tried adding the Bulkhead, removing the Bulkhead. I've tried adding additional fallback methods with different signatures. I've tried with and without @Component.

All I end up getting in my logs is this 100 times:

14:33:10.348 [main] INFO c.t.circuits.CircuitsApplication - Error caught in main loop

I'm not sure what I'm missing. Any help would be greatly appreciated.

Jeremy Smith
  • 311
  • 2
  • 13

1 Answers1

3

I don't think this will work. Firstly, you are instantiating your ResilientClient as new ResilientClient(). You have to use the created Bean not instantiate it yourselves. The @CircuitBreaker annotation uses spring-aop. So you will have to run your class as SpringBootApplicaiton.

Secondly, you are only recording HttpServerErrorException and IOException as failures. So circuit breaker treats all other exceptions (except the ones mentioned above and their children) as success.

Akhil Bojedla
  • 1,968
  • 12
  • 19
  • To be clear, the above code was me pulling the code out of my Spring Boot application. :) Right now, the client is instantiated from the service layer in the constructor using ```(@Autowired ResilientClient resilientClient)``` There are several items for using Resilience4J in Spring Boot 2 that are not covered in the documentation, including needing to have Spring autowire the bean or setting up a config class and the example Java code in the docs has an undefined variable – Jeremy Smith Oct 16 '19 at 14:20
  • 1
    Try adding `java.lang.Throwable` to `resilience4j.circuitbreaker.configs.default. recordExceptions` if you want to retry on all exceptions. You can add ignored exceptions to `resilience4j.circuitbreaker.configs.default. ignoreExceptions` – Akhil Bojedla Oct 16 '19 at 15:07
  • 2
    By default all exceptions count as a failure. You don't have to add Throwable to recordExceptions. You just have to add exceptions to the list of ignoreExpections. – Robert Winkler Oct 18 '19 at 11:56
  • Yes if you don't configure the `recordExceptions` they do. But if you put a list of exceptions in `recordExceptions` config they don't. `recordExceptions`: A list of exceptions that are recorded as a failure and thus increase the failure rate. Any exception matching or inheriting from one of the list counts as a failure, unless explicitly ignored via ignoreExceptions. If you specify a list of exceptions, all other exceptions count as a success, unless they are explicitly ignored by ignoreExceptions. – Akhil Bojedla Oct 18 '19 at 11:59
  • 2
    I know it. I wrote it. :) If you don't specify a list of exceptions which should be recorded. All exceptions are recorded unless they are explicity ignored. – Robert Winkler Oct 18 '19 at 12:00
  • @Jeremy Smith Which items are missing in the documentation? You can create a GitHub issue if something is missing for you. – Robert Winkler Oct 18 '19 at 12:11
  • @RobertWinkler, specifically I'm talking about the documentation here: https://resilience4j.readme.io/docs/getting-started-3 so I'm not sure how you'd want that opened in GitHub. :) In your annotation section, the variable BACKEND is not defined within the example code. There is no mention that the class being annotated has to be declared as a component or have the bean defined in a configuration file - it mentions only that it should be configured in application.yml. While the sample code in GitHub has this, the documentation does not. – Jeremy Smith Oct 18 '19 at 13:57
  • @RobertWinkler there were some other steps I had to take to get things working in addition, but I'd have to go back through. Additionally, to get the sample project working, I had to change the gradle wrapper and the build.gradle, as I'm using IntelliJ IDEA, not Eclipse, although I didn't really consider that to be a major issue, as that's really dependent on the particular individual. Having a POM-based example might also be helpful. – Jeremy Smith Oct 18 '19 at 13:59
  • @AkhilBojedla please help me https://stackoverflow.com/questions/68407909/using-circuit-breaker-in-spring-boot – Akash Sharma Jul 16 '21 at 12:27