You can configure retries in Spring applications several different ways. Two popular ways to configure retries are using Spring Retry and Resilience4j Spring Boot starter. While these are popular, I ran into some limitations with using these tools. This tutorial explains those limitations and how you can work around them.
Limitation of Spring Retry and Spring Boot Resilience4j
A limitation I observed with Spring Retry and Spring Boot Resilience4j is that they work if the method you want to retry is at the top level of the class.
For example, it works for an @Controller
annotated class A to call a method B1 in @Service
annotated class B. However, it does not work to retry a method B2 that was called from B1.
The reason for this is that Spring-retry uses the @Retryable
annotation to automatically re-invoke a failed operation. @Retryable
is implemented using Spring AOP which means only external calls to retriable methods go through the proxy. Internal calls within the class bypass the proxy and, therefore, are not retried.
Steps to add the Retry
capability using Resilience4j
-
Add the resilience4j dependency in
build.gradle
, like so:1implementation "io.github.resilience4j:resilience4j- retry:${resilience4jVersion}"
-
Define the
Retry
bean in the application configuration.
The following configuration attempts a retry three times including the initial attempt. It attempts retry just for ObjectOptimisticLockingFailureException
. It will wait for 300 milli seconds between the retries. If all retries are exhausted, an error is thrown.
@Bean
public Retry retry() {
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(300))
.retryExceptions(ObjectOptimisticLockingFailureException.class)
.failAfterMaxAttempts(true)
.build();
// Create a RetryRegistry with a custom global configuration
RetryRegistry registry = RetryRegistry.of(config);
return registry.retry( name:”dbRetry”);
}
}
-
Define the Java 8 function for inner method
retryWith
. For example, this::retryWith
. -
The function defined in the code listing above can be decorated with Retry configured as below.
this.retryableFunction = Retry.decorateFunction(retry, this::retryWith);
-
Call the
Retryable
method as shown below:ScanRequest scanRequest = ScanRequest.builder() .entity(newScanRecordEntity) .eventType(scanCallbackRequest.getEventType()) .build(); //Try updating DB with retry. ScanRecordEntity scanRecordEntity = retryableFunction.apply(scanRequest);
Conclusion
Using these steps above, I was able to avoid refactoring my code. More importantly, I did not have to change any tests but were still able to achieve the retry mechanism as intended. I hope this helps you save time in troubleshooting and implementing retry for inner methods.