Have you ever come across a situation where you wanted to retry a method invocation automatically?
Let's say you are calling a stock ticker service for a given stock and get a transient error. Since it is a transient error, you will try again and it may work in second attempt.
But what if it doesn't? Well, you will try third time. But how many times can you try like that? More importantly after how much time will you retry?
Imagine if you have a handful of methods like this. Your code will become convoluted with retry logic. Is there a better way?
Well, if you are using spring/spring boot, you are in luck. Here is how you can do that using spring. Let's write our business service as follows.
import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class StockService {
int count;
@Retryable(value = RuntimeException.class,
backoff = @Backoff(random = true, delay = 1000, maxDelay = 5000, multiplier = 3))
public LocalDateTime getStockUpdatedTime(String stock) {
count++;
log.info("Inside StockService {}", count);
if (count < 2) {
throw new RuntimeException();
}
return LocalDateTime.now();
}
}
Following is the application class.
import java.time.LocalDateTime;
import java.util.concurrent.ExecutionException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableRetry
@Slf4j
public class Application {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
StockService bean = context.getBean(StockService.class);
log.info("main {}", LocalDateTime.now());
LocalDateTime stockUpdatedAt = bean.getStockUpdatedTime('ABC');
log.info("stockUpdatedAt {}", stockUpdatedAt);
log.info("main {}", LocalDateTime.now());
}
}
Here,
- Mark your method with @Retryable.
- Add @EnableRetry to enable the auto scanning of the above annotation.
- value refers to the exception upon which you want to retry the method invocation.
- maxAttempts defines the number of retries, the default is 3.
- @Backoff referes to the retry expression. Here we are attempting the method invocation 3 times with the following delay computation logic. Min delay of 1 sec and max delay of 5 sec and the generated random delay is then multiplied by the multiplier and used in each retry.
What happend to the caller during the retry? Will it wait until all the retries?
Yes, in this example, the output from main method is printed as if @Retryable was not used at all; i.e., the last two log lines are printed after the call from getStockUpdatedTime() returns.
Wow! this helps. Wait... what happens if your method is within a transaction boundry? i.e., if the method that you want to retry is part of a db transaction.
There are two possibilities.
- The method that you want to retry is annotated with @Transactional annotation.
- The caller of the method you want to retry is annotated with @Transactional annotation.
@Retryable(value = RuntimeException.class,
backoff = @Backoff(random = true, delay = 1000, maxDelay = 5000, multiplier = 3))
@Async
public CompletableFuture<LocalDateTime> getStockUpdatedTime(String stock) {
count++;
log.info("Inside StockService {}", count);
if (count < 2) {
throw new RuntimeException();
}
return CompletableFuture.completedFuture(LocalDateTime.now());
}
log.info("main {}", LocalDateTime.now());
LocalDateTime stockUpdatedAt = bean.getStockUpdatedTime('ABC').get();
log.info("getStockUpdatedTime {}", stockUpdatedAt);
log.info("main {}", LocalDateTime.now());
Comments
Post a Comment