Skip to main content

Auto retries in REST API clients On Java On Ease

 


Writing REST clients to consume API endpoints has become commonplace. While consuming REST endpoints, we sometimes end up in a situation where a downstream service throws some kind of transient error that goes away when the API call is retried. In such situations, we ask ourselves — “What if my API client was smart enough that knew how to retry a failed call?”
Some of us go the extra mile to implement our own custom code that can retry API calls on error. But mind you, it is not only about knowing how to retry. The client also has to know when to retry and when not to. If the error is irrecoverable such as 400 — Bad Request, there is no point in retrying. It might also have to know how to back off and how to recover from the error. Implementing all this by hand, and then repeating it over and over again in every API client is cumbersome. It also adds a lot of boilerplate code and makes things even worse.
But if you are a Spring/Spring Boot developer, you will be surprised to know how easy it is to implement the retry mechanism using Spring Retry. In fact, Spring has implemented everything. It is just about knowing how to wire up everything together to get the retryable REST client.
With Spring Retry, you can retry pretty much everything. I am, however, only going to show you today how to implement a base REST API client that is capable of retrying API calls.
I am using a Spring Boot project to demonstrate this. You can find the entire source code on my Github. For brevity, I am only posting important snippets in this post. Also, I am going to use programmatic way of implementing retries. Spring also provides a declarative way using annotations however I think programmatic approach gives better control.
Let’s start by adding the following gradle dependency to our Spring Boot project. You can replace it with equivalent maven dependency if you use maven.
implementation 'org.springframework.retry:spring-retry'
Next, we will create a very simple RestTemplate bean that will be used to invoke the REST calls.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Now the important part. We have to create the retry configuration. Spring Retry provides a template class Retry Template that simplifies the retry operation. By default, the operation is retried if it throws java.lang.Exception or any of its subclasses. However, we want to achieve more granular control on when to retry the operation. We will implement our logic to retry only when the HTTP response status code is one of:

500 Internal Server Error
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
For that purpose, we have to explicitly set the retry policy. Spring Retry provides many retry policies out of the box. I am going to use ExceptionClassifierRetryPolicy which gives us an opportunity to classify the exceptions and decide whether to retry or not based on the possibility of recovering from the error.

import org.springframework.classify.Classifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.policy.ExceptionClassifierRetryPolicy;
import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.HttpStatusCodeException;

@Configuration
public class RetryConfig {
private static final int MAX_RETRY_ATTEMPTS = 2;
private final SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(MAX_RETRY_ATTEMPTS);
private final NeverRetryPolicy neverRetryPolicy = new NeverRetryPolicy();

@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
ExceptionClassifierRetryPolicy policy = new ExceptionClassifierRetryPolicy();
policy.setExceptionClassifier(configureStatusCodeBasedRetryPolicy());
retryTemplate.setRetryPolicy(policy);
return retryTemplate;
}

private Classifier<Throwable, RetryPolicy> configureStatusCodeBasedRetryPolicy() {
return throwable -> {
if (throwable instanceof HttpStatusCodeException) {
HttpStatusCodeException exception = (HttpStatusCodeException) throwable;
return getRetryPolicyForStatus(exception.getStatusCode());
}
return simpleRetryPolicy;
};
}

private RetryPolicy getRetryPolicyForStatus(HttpStatus httpStatus) {
switch (httpStatus) {
case BAD_GATEWAY:
case SERVICE_UNAVAILABLE:
case INTERNAL_SERVER_ERROR:
case GATEWAY_TIMEOUT:
return simpleRetryPolicy;
default:
return neverRetryPolicy;
}
}
}
Create an object of SimpleRetryPolicy with a desirable number of retries. I’ve set MAX_RETRY_ATTEMPTS = 2. This policy will be used when one of the aforementioned HTTP status codes is received from the downstream service and we want to retry the API call. Note that the retry count also includes the original failed invocation.
For all other status codes, create and use NeverRetryPolicy.
Now create an ExceptionClassifier where we will write the actual HTTP status code logic. Spring REST client raises one of the subclasses of HttpStatusCodeException when downstream service returns an erroneous response. Retrieve the status code from it and then write the switch case.
Now we’ve come to the final part of writing our BaseRetryableApiClient. I have only implemented get however all other HTTP methods can be implemented similarly.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
public class BaseRetryableApiClient {

@Autowired
private RetryTemplate retryTemplate;

@SuppressWarnings("rawtypes")
protected final <T> T get(RestTemplate restTemplate, String url, HttpEntity httpEntity,
Class<T> responseType, Object... urlVariables) {
ResponseEntity<T> responseEntity = retryTemplate
.execute(context -> restTemplate.exchange(url, HttpMethod.GET, httpEntity,
responseType, urlVariables));
return responseEntity.getBody();
}
}
Now our concrete API clients can extend from this BaseRetryableApiClient and invoke the get method provided by superclass which by default has the retry mechanism built-in.
Here’s an example of a concrete API client —

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
public class ImdbRapidApiClient extends BaseRetryableApiClient {
private static final String URL = "https://imdb8.p.rapidapi.com/title/auto-complete?q=deathly%20hallows";
@Autowired
private RestTemplate restTemplate;
@Value("${rapid-api-key}")
private String rapidApiKey;
public String getImdbTitle() {
HttpHeaders headers = new HttpHeaders();
headers.add("x-rapidapi-host", "imdb8.p.rapidapi.com");
headers.add("x-rapidapi-key", rapidApiKey);
return get(restTemplate, URL, new HttpEntity<>(headers), String.class);
}
}



Credit :  Github repository

Comments

Popular posts from this blog

How to create Annotation in Spring boot

 To create Custom Annotation in JAVA, @interface keyword is used. The annotation contains :  1. Retention :  @Retention ( RetentionPolicy . RUNTIME ) It specifies that annotation should be available at runtime. 2. Target :  @Target ({ ElementType . METHOD }) It specifies that the annotation can only be applied to method. The target cane be modified to:   @Target ({ ElementType . TYPE }) for class level annotation @Target ({ ElementType . FIELD }) for field level annotation @Retention ( RetentionPolicy . RUNTIME ) @Target ({ ElementType . FIELD }) public @ interface CustomAnnotation { String value () default "default value" ; } value attribute is defined with @ CustomAnnotation annotation. If you want to use the attribute in annotation. A single attribute value. Example : public class Books {           @CustomAnnotation(value = "myBook")     public void updateBookDetail() {         ...

Kafka And Zookeeper SetUp

 Kafka And Zookeeper SetUp zookeeper download Link : https://www.apache.org/dyn/closer.lua/zookeeper/zookeeper-3.8.3/apache-zookeeper-3.8.3-bin.tar.gz Configuration: zoo.conf # The number of milliseconds of each tick tickTime =2000 # The number of ticks that the initial # synchronization phase can take initLimit =10 # The number of ticks that can pass between # sending a request and getting an acknowledgement syncLimit =5 # the directory where the snapshot is stored. # do not use /tmp for storage, /tmp here is just # example sakes. dataDir =/tmp/zookeeper # the port at which the clients will connect clientPort =2181 4 char whitelist in command arguments 4lw.commands.whitelist =* Start ZooKeeper Server $ bin/zkServer.sh start Check zookeeper status dheeraj.kumar@Dheeraj-Kumar bin % echo stat | nc localhost 2181 stat is 4 character whitelisted argument  Check Kafka running status : echo dump | nc localhost 2181 | grep broker Responsibility of Leader in Zookeeper: 1. Distrib...

Cache Policy

Cache policies determine how data is stored and retrieved from a cache, which is a small and fast storage area that holds frequently accessed data to reduce the latency of accessing that data from a slower, larger, and more distant storage location, such as main memory or disk. Different cache policies are designed to optimize various aspects of cache performance, including hit rate, latency, and consistency. Here are some common types of cache policies: Least Recently Used (LRU): LRU is a commonly used cache replacement policy. It evicts the least recently accessed item when the cache is full. LRU keeps track of the order in which items were accessed and removes the item that has not been accessed for the longest time. First-In-First-Out (FIFO): FIFO is a simple cache replacement policy. It removes the oldest item from the cache when new data needs to be stored, regardless of how frequently the items have been accessed. Most Recently Used (MRU): MRU removes the most recently accessed ...