Recentemente, assumi a manutenção de um Inicialização do Spring projeto. Esse projeto tem algumas RateLimit erros nos logs quando o aplicativo estava entrando em contato com uma API REST remota. Acontece que esse aplicativo também estava usando a API síncrona e bloqueadora RestTemplate para fazer as chamadas de API, em vez do mais recente Spring Cliente Webque, por acaso, está usando a API do Reactor por trás do capô.
E você sabe o que é ótimo no Reactor e no uso de APIs reativas em geral? Ele facilita muito a programação com fluxo de dados. O que também significa que facilita a implementação de estratégias de repetição.
Vamos falar primeiro sobre o erro no registro. O que recebi foi o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
org.springframework.web.client.HttpClientErrorException$TooManyRequests: 429 Too Many Requests: [{"response_type":"ERROR","message":"Number of requests has exceeded the 1 minute limit"}] at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:137) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:184) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:125) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:782) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:740) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:674) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:315) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at com.couchbase.training.couchlms.repository.LmsRepository.getCourseModules(LmsRepository.java:102) ~[classes!/:0.0.40-SNAPSHOT] at com.couchbase.training.couchlms.services.LmsProcessor.processCourseModules(LmsProcessor.java:147) [classes!/:0.0.40-SNAPSHOT] at com.couchbase.training.couchlms.services.LmsProcessor.processCourses(LmsProcessor.java:91) [classes!/:0.0.40-SNAPSHOT] at com.couchbase.training.couchlms.config.SchedulingConfig.scheduledCoursesPuller(SchedulingConfig.java:45) [classes!/:0.0.40-SNAPSHOT] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_252] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_252] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_252] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_252] at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) [spring-context-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) [spring-context-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_252] at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308) [na:1.8.0_252] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_252] at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) [na:1.8.0_252] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_252] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_252] at java.lang.Thread.run(Thread.java:748) [na:1.8.0_252] |
Como de costume, a parte interessante dos rastreamentos de pilha está na parte superior. O erro é 429 Solicitações em excessoe a mensagem diz que há um Limite de taxa de 1 minuto. Decompondo isso, o status HTTP O código retornado é 429. É um erro de limite de taxa, o que significa que a API informa ao chamador que enviou um número excessivo de solicitações. Normalmente, isso pode ser resolvido esperando um pouco, e você pode até ter um Tentar novamente depois na resposta informando quanto tempo você precisa esperar.
Vamos ver como podemos obter essas informações com o WebClient do Spring:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.uri(ub -> ub.pathSegment(uri).queryParams(queryParams).build()) .retrieve() .onStatus( HttpStatus.TOO_MANY_REQUESTS::equals, response -> { List<String> header = response.headers().header("Retry-After"); Integer delayInSeconds; if (!header.isEmpty()) { delayInSeconds = Integer.valueOf(header.get(0)); } else { delayInSeconds = 60; } return response.bodyToMono(String.class).map(msg -> new RateLimitException(msg, delayInSeconds)); }) .bodyToMono(String.class) |
Esse código está enviando um OBTER solicitação. O WebClient nos permite dar uma olhada na resposta da solicitação e reagir adequadamente graças à função onStatus method. O primeiro parâmetro é um booleano usado para filtrar o código de status HTTP retornado. Aqui, quando o código de status é 429, fazemos alguma coisa.
Vamos dar uma olhada no Resposta veja se há um cabeçalho Tentar novamente depois se for o caso, inicializamos o cabeçalho delayInSeconds com ela; caso contrário, definimos um valor padrão de 60. Em seguida, podemos enviar de volta o Resposta mapeado para um corpo RateLimitException. O conteúdo do corpo será usado como uma mensagem de erro e o delayInSeconds estará disponível em um campo separado. Dê uma olhada no código da Exceção para obter mais detalhes:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import java.time.Duration; public class RateLimitException extends Throwable { private int retryAfterDelay = 60; public RateLimitException(String message) { super(message); } public RateLimitException(String message, int retryAfterDelay) { super(message); this.retryAfterDelay = retryAfterDelay; } public int getRetryAfterDelay() { return retryAfterDelay; } public Duration getRetryAfterDelayDuration() { return Duration.ofSeconds(retryAfterDelay); } } |
Portanto, o que precisa ser feito é capturar esse erro específico e tentar novamente após o período determinado. O Reactor facilita isso fornecendo Novas tentativas estratégias. Tudo o que você precisa fazer é chamar o retryWhen método:
|
1 2 3 4 5 6 |
.bodyToMono(String.class) .retryWhen(Retry.withThrowable(throwableFlux -> { return throwableFlux.filter(t -> t instanceof RateLimitException).map(t -> { RateLimitException rle = (RateLimitException) t; return Retry.fixedDelay(1, rle.getRetryAfterDelayDuration()); }); |
Existem diferentes Repetir aqui podemos usar os métodos withThrowable() construtor. Ele fornece um Flux que deve conter o RateLimitException. Portanto, começamos aplicando um filtro para nos certificarmos disso. Em seguida, mapeamos essa exceção para o objeto Retry real. Aqui é o objeto Retry.fixedDelay tomando como parâmetros as tentativas máximas e a duração. A duração vem do RateLimitException que foi lançado anteriormente.
Com isso, toda vez que uma solicitação retornar um 429, o cliente aguardará o tempo apropriado até tentar novamente. E foi muito mais fácil implementar com o Reactor do que usar o try/catch com o RestTemplate. Sei que a programação reativa pode ser um pouco intimidadora no início, mas é uma ótima maneira de gerenciar fluxos de dados, como solicitações e respostas HTTP, ou de gerenciar conexões com bancos de dados que suportam programação reativa, como Couchbase.
Deseja mais ajuda e ideias?