HTTP Retry with Exponential Backoff in Java and Spring Boot
Implementing HTTP retry with exponential backoff in spring boot is not trivial. Let's learn it in this blog post
In this blog post, we will learn to implement the HTTP retry mechanism in java and spring boot.
Rest Template
Rest template is the popular HTTP client in spring boot. We will create a configuration and create a bean for it so that the spring boot's container picks it up.
package com.abc.config;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate =
new RestTemplateBuilder().setConnectTimeout(Duration.ofSeconds(30))
.setReadTimeout(Duration.ofSeconds(90)).build();
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
restTemplate.setInterceptors(interceptors);
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
return restTemplate;
}
}
Retry Config
We need to add spring-retry
and spring-aspects
to our dependencies.
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
package com.abc.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.HttpServerErrorException;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RetryConfig {
ExceptionClassifierBackoffPolicy exceptionClassifierBackoffPolicy;
@Autowired
public RetryConfig(ExceptionClassifierBackoffPolicy exceptionClassifierBackoffPolicy) {
this.exceptionClassifierBackoffPolicy = exceptionClassifierBackoffPolicy;
}
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(5);
ExponentialBackOffPolicy exponentialBackOffPolicy = new ExponentialBackOffPolicy();
exponentialBackOffPolicy.setInitialInterval(5000L);
exponentialBackOffPolicy.setMaxInterval(40000L);
Map<Class<? extends Throwable>, BackOffPolicy> policyMap = new HashMap<>();
policyMap.put(HttpServerErrorException.class, exponentialBackOffPolicy);
exceptionClassifierBackoffPolicy.setPolicyMap(policyMap);
retryTemplate.setRetryPolicy(simpleRetryPolicy);
retryTemplate.setBackOffPolicy(exceptionClassifierBackoffPolicy);
return retryTemplate;
}
}
ExponentialBackoffPolicy
takes the following parameters.
initialinterval
- The initial number of seconds to wait.
maxInterval
- The maximum number of seconds to wait.
multiplier
- The factor to multiply the initial interval with.
We created a hashmap of the exception class and the policy to use and then we added it to our rest template. A point to note here is after setting the exponential backoff policy, the simple retry policy also needs to be set. If it is not set, by default only 3 retries will be made. So, in the above code, we set the max retires as 5 to the simple retry policy because, in the exponential back-off policy, we set 5 seconds as the initial delay. So, on the first retry, it waits for 5, then 10, 20, and 40. Totally 5 retries will be made. Make sure to adjust these numbers that works better for you.
ExceptionClassifierBackoffPolicy
There is a default ExceptionClassifierRetryPolicy
using which we can configure policies for exception classes. But we cannot use ExponentialBackOffPolicy
it. Because in spring retry, Retry
and Backoff
are two different policies implementing different interfaces. So, we need to create our own custom ExceptionClassifierBackoffPolicy
class for it.
package com.abc.config;
import org.springframework.classify.Classifier;
import org.springframework.classify.ClassifierSupport;
import org.springframework.classify.SubclassClassifier;
import org.springframework.retry.RetryContext;
import org.springframework.retry.backoff.BackOffContext;
import org.springframework.retry.backoff.BackOffInterruptedException;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.NoBackOffPolicy;
import java.util.HashMap;
import java.util.Map;
public class ExceptionClassifierBackoffPolicy implements BackOffPolicy {
private static class ExceptionClassifierBackoffContext implements BackOffContext, BackOffPolicy {
Classifier<Throwable, BackOffPolicy> exceptionClassifier;
RetryContext retryContext;
Map<BackOffPolicy, BackOffContext> contextMap = new HashMap<>();
ExceptionClassifierBackoffContext(Classifier<Throwable, BackOffPolicy> exceptionClassifier, RetryContext retryContext) {
this.exceptionClassifier = exceptionClassifier;
this.retryContext = retryContext;
}
@Override
public BackOffContext start(RetryContext context) {
return (BackOffContext) context;
}
@Override
public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
BackOffPolicy policy = exceptionClassifier.classify(retryContext.getLastThrowable());
BackOffContext policyContext = contextMap.get(policy);
if (policyContext == null) {
policyContext = policy.start(retryContext);
contextMap.put(policy, policyContext);
}
policy.backOff(policyContext);
}
}
private Classifier<Throwable, BackOffPolicy> exceptionClassifier =
new ClassifierSupport<>(new NoBackOffPolicy());
public void setPolicyMap(Map<Class<? extends Throwable>, BackOffPolicy> policyMap) {
this.exceptionClassifier = new SubclassClassifier<>(policyMap, new NoBackOffPolicy());
}
@Override
public BackOffContext start(final RetryContext context) {
return new ExceptionClassifierBackoffContext(exceptionClassifier, context);
}
@Override
public void backOff(final BackOffContext backOffContext) throws BackOffInterruptedException {
ExceptionClassifierBackoffContext classifierBackOffContext = (ExceptionClassifierBackoffContext) backOffContext;
classifierBackOffContext.backOff(backOffContext);
}
}
Main class
Now combining all of it, here's the main class.
package com.abc;
import com.abc.config.ExceptionClassifierBackoffPolicy;
import com.abc.config.RestTemplateConfig;
import com.abc.config.RetryConfig;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
public class main {
public static void main(String[] args) {
RetryConfig retryConfig = new RetryConfig(new ExceptionClassifierBackoffPolicy());
RestTemplateConfig restTemplate = new RestTemplateConfig();
HttpEntity<String> resp = new HttpEntity<>(null);
ResponseEntity<String> newResp =
retryConfig.retryTemplate().execute(context -> restTemplate.restTemplate().exchange(
"https://abc-test" +
".free" +
".beeceptor" +
".com/test/abc",
HttpMethod.GET,
resp,
String.class));
System.out.println(newResp.getBody());
}
}