javaspringspring-bootresilience4jresilience4j-retry

What is the expected behavior of Resilience4J Retry?


I'm using Resilience4J in a Spring Boot project to make a call to a REST client like so:

@Retry(name = "customerService")
public Customer getCustomer(String customerNumber) {
    restTemplate.exchange("https://customer-service.net", HttpMethod.GET, ..., customerNumber);
}

With this configuration:

resilience4j.retry:
    instances:
        customerService:
            maxAttempts: 3
            waitDuration: 10s
            retryExceptions:
                - org.springframework.web.client.HttpServerErrorException

My expectation is if restTemplate.exchange() is invoked and the customer service responds with a HttpServerErrorException, the getCustomer() method will be called 3 more times after a wait of ten seconds.

However, this is not the case.

This method is never called again, and the exception is immediately thrown.

Seeing that a fallback method is included in the examples, I decided to add it, even though I don't really want to invoke a different method, I just want my original method called again.

Anyway, I specified a fall back:

@Retry(name = "customerService", fallback = "customerFb")
public Customer getCustomer(String customerNumber) {
    return getCustomer();
}

private Customer customerFb(String customerNumber, HttpServerErrorException ex) {
    log.error("Could not retrieve customer... retrying...");
    return getCustomer();
}

private Customer getCustomer(String customerNumber) {
    return restTemplate.exchange("https://customer-service.net", HttpMethod.GET, ..., customerNumber);
}

Now, I am seeing the fallback method being retried, however, the HttpServerErrorException is being thrown each time, meaning that consumers will receive an exception as a response to their calls.

My questions are:

Does a fallback method need to be implemented in order for retry functionality to work?

and

Is it expected behavior for the exceptions to be thrown? Am I doing something wrong? I don't want callers of this service to receive an exception until all retry attempts have been made.

Thanks


Solution

  • Q: Does a fallback method need to be implemented in order for retry functionality to work?

    Ans: No, Fallback is optional in Resilience4J Retry feature.

    Behaviour of Resilience4J Retry:

    1. At startup, resilience retry loads the configuration from application.properties/yml if configured else will initialize with default values as mentioned here in the documentation.

    2. When a method annotated with @Retry is invoked, then it will be under retry monitor.

    3. If property resilience4j.retry.instances.<instance name>.retryExceptions is configured explicitly then only the configured exceptions will be considered as a failure and retry will be triggered for only these failures and for the rest of the exceptions it behaves normal without the retry feature.

    4. When an expected exception is occurred under @retry annotated method, then it won't log anything about the exception but it retries the same method as per the maxAttempts property configured. Default value of maxAttempts property is 3. Ater the maxAttempts it throws the exceptions and can be seen in the log.

    5. There are also properties like waitDuration, intervalFunction, ignoreExceptions.. e.tc which can be explicitly configured. For more look at the documentation link.

    6. To see actually what's happening during an event of exception in the method annotated with @retry, Enable the RetryRegistry event consumer to print the events received to it.

    Example Code for RetryRegistry event listener:

    package com.resilience.retry.config;
    
    import javax.annotation.PostConstruct;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import io.github.resilience4j.retry.RetryRegistry;
    import lombok.extern.slf4j.Slf4j;
    
    @Component
    @Slf4j
    public class RetryRegistryEventListener {
    
        @Autowired
        private RetryRegistry registry;
    
    
        @PostConstruct
        public void postConstruct() {
            //registry.retry(<resilience retry instance name>)
            registry.retry("sample-api").getEventPublisher()
                    .onRetry(ev -> log.info("#### RetryRegistryEventListener message: {}", ev));
        }
    }
    

    example log:

    2021-11-01 14:55:12.337  INFO 18176 --- [nio-8080-exec-1] c.r.retry.controller.RetryController     : Sample Api call receieved
    2021-11-01 14:55:12.345  INFO 18176 --- [nio-8080-exec-1] c.r.r.config.RetryRegistryEventListener  : #### RetryRegistryEventListener message: 2021-11-01T14:55:12.344325600+05:30[Asia/Calcutta]: Retry 'sample-api', waiting PT10S until attempt '1'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 500 INTERNAL_SERVER_ERROR'.
    2021-11-01 14:55:12.350  INFO 18176 --- [nio-8080-exec-1] c.r.retry.controller.RetryController     : messsage: 2021-11-01T14:55:12.344325600+05:30[Asia/Calcutta]: Retry 'sample-api', waiting PT10S until attempt '1'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 500 INTERNAL_SERVER_ERROR'.
    2021-11-01 14:55:22.359  INFO 18176 --- [nio-8080-exec-1] c.r.retry.controller.RetryController     : Sample Api call receieved
    2021-11-01 14:55:22.360  INFO 18176 --- [nio-8080-exec-1] c.r.r.config.RetryRegistryEventListener  : #### RetryRegistryEventListener message: 2021-11-01T14:55:22.360672900+05:30[Asia/Calcutta]: Retry 'sample-api', waiting PT10S until attempt '2'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 500 INTERNAL_SERVER_ERROR'.
    2021-11-01 14:55:22.361  INFO 18176 --- [nio-8080-exec-1] c.r.retry.controller.RetryController     : messsage: 2021-11-01T14:55:22.360672900+05:30[Asia/Calcutta]: Retry 'sample-api', waiting PT10S until attempt '2'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 500 INTERNAL_SERVER_ERROR'.
    

    @FerdTurgusen: I believe there is some mistake in your code else it would be working fine. With the given information in the question i was unable to exactly find the issue. Hence, I have created a sample spring boot with resilience4j example, replicated your issue and it's working fine for me, hence uploaded to Github for your reference. I suggest you to add RetryRegistryEventListener class and find the issue in event logs, if still not resolved please share the event listener log.

    Github Reference: sample spring-boot with resilience4j retry project