How to make pause between each api call in springframework RestClient

63 views Asked by At

This is a org.springframework.web.client.RestClient config

@Configuration
public class CloudConfig {
    @Value("${experimental.host}")
    String baseURI;
    @Bean
    RestClient restClient(RestClient.Builder builder){
        return builder.baseUrl(baseURI).build();
    }
}

This is service which does a call to external API

@Component
public class RequestService {

    public String callAPI(int userID){
        Map<String, Object> properties = Map.of("id", userID);
        return restClient.post()
                .uri("/external")
                .body(properties)
                .retrieve()
                .body(String.class);
    }
}

I get a list of user from db and in the loop call external API

@Service
public class RabbitMQProducer {

    private UserRepository repository;
    private RequestService requestService;

    @Scheduled(fixedRate = 10000)
    public void sendUserData(){
        for(User user : repository.findAll()) {
            String data = requestService.callAPI(user.getID);
            ......
        }
    }   
}

What is a correct way to make a pause between each call because external api has constraine of 1 second to call ? I got error message from API "org.springframework.web.client.HttpClientErrorException$TooManyRequests: too many request 2 times in 1000 millseconds"

Is there any pattern or solution how to resolve that kind of problem ?

I think expected behavior:

call API --> 1 second wait --> call API --> 1 second wait ...

Easy way to fix that it's just to add Thread.sleep(1000) but i'm not sure that is a good solution

2

There are 2 answers

2
johyunkim On BEST ANSWER

If you use Thread.sleep(1000), then it will make constraints of 1 call for 1 second, but it is not ideal because it will block the current thread.

1. Webflux & webClient

I use this.

   public Mono<String> callAPI(User user) {
        return webClient.post()
                .uri("/external")
                .bodyValue(Map.of("id", user.getID()))
                .retrieve()
                .bodyToMono(String.class)
                .delayElement(Duration.ofSeconds(1)); // Delay for each subscriber
    }

2. Resilience4j

This is the rate-limiting library.

import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import java.time.Duration;

@Configuration
public class RateLimiterConfig {

    @Bean
    public RateLimiter rateLimiter() {
        RateLimiterConfig config = RateLimiterConfig.custom()
            .limitRefreshPeriod(Duration.ofSeconds(1))
            .limitForPeriod(1)
            .timeoutDuration(Duration.ofMillis(500))
            .build();

        return RateLimiter.of("apiRateLimiter", config);
    }
}

.

@Service
public class RateLimitedRequestService {

    private final RequestService requestService;
    private final RateLimiter rateLimiter;

    public RateLimitedRequestService(RequestService requestService, RateLimiter rateLimiter) {
        this.requestService = requestService;
        this.rateLimiter = rateLimiter;
    }

    public String callAPI(User user) {
        return RateLimiter.decorateSupplier(rateLimiter, () -> requestService.callAPI(user.getID())).get();
    }
}

2. @Async

You should add @EnableAsync

@Async
public CompletableFuture<String> callAPIWithDelay(User user, long delay) {
    return CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(delay);
            return requestService.callAPI(user.getID());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }
    });
}

+a, retry

I recommend you add retry to ensure that works properly. When working with external API that has a rate limiting, it is recommended to make some retry logic.

0
hyun On

it is recommended to create a separate thread and do the work.

@Async
public CompletableFuture<String> callAPIWithDelay(int userID, long delay) {
    return CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(delay);
            Map<String, Object> properties = Map.of("id", userID);
            return restClient.post()
                .uri("/external")
                .body(properties)
                .retrieve()
                .body(String.class);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }
    });
}

Using @Async creates a thread each time it is called, so it is recommended to use a threadpool. If you make the settings below, SpringBoot uses ThreadPoolTaskExecutor through autoConfiguration.

spring:
  task:
    execution:
      pool:
        core-size: 5
        max-size: 5
        queue-capacity: 5
        keep-alive: 30s