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.estrutura de mola.web.cliente.HttpClientErrorException$TooManyRequests: 429 Também Muitos Solicitações: [{"response_type":"ERROR","mensagem":"O número de solicitações excedeu o limite de 1 minuto"}] em org.estrutura de mola.web.cliente.HttpClientErrorException.criar(HttpClientErrorException.java:137) ~[mola-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em org.estrutura de mola.web.cliente.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:184) ~[mola-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em org.estrutura de mola.web.cliente.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:125) ~[mola-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em org.estrutura de mola.web.cliente.Manipulador de erros de resposta.handleError(Manipulador de erros de resposta.java:63) ~[mola-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em org.estrutura de mola.web.cliente.RestTemplate.handleResponse(RestTemplate.java:782) ~[mola-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em org.estrutura de mola.web.cliente.RestTemplate.doExecute(RestTemplate.java:740) ~[mola-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em org.estrutura de mola.web.cliente.RestTemplate.executar(RestTemplate.java:674) ~[mola-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em org.estrutura de mola.web.cliente.RestTemplate.getForObject(RestTemplate.java:315) ~[mola-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em com.couchbase.treinamento.couchlms.repositório.LmsRepository.getCourseModules(LmsRepository.java:102) ~[aulas!/:0.0.40-FOTOGRAFIA] em com.couchbase.treinamento.couchlms.serviços.LmsProcessor.processCourseModules(LmsProcessor.java:147) [aulas!/:0.0.40-FOTOGRAFIA] em com.couchbase.treinamento.couchlms.serviços.LmsProcessor.processCourses(LmsProcessor.java:91) [aulas!/:0.0.40-FOTOGRAFIA] em com.couchbase.treinamento.couchlms.configuração.SchedulingConfig.scheduledCoursesPuller(SchedulingConfig.java:45) [aulas!/:0.0.40-FOTOGRAFIA] em sol.refletir.NativeMethodAccessorImpl.invocar0(Nativo Método) ~[na:1.8.0_252] em sol.refletir.NativeMethodAccessorImpl.invocar(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_252] em sol.refletir.DelegatingMethodAccessorImpl.invocar(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_252] em java.lang.refletir.Método.invocar(Método.java:498) ~[na:1.8.0_252] em org.estrutura de mola.agendamento.suporte.ScheduledMethodRunnable.executar(ScheduledMethodRunnable.java:84) [mola-contexto-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em org.estrutura de mola.agendamento.suporte.DelegatingErrorHandlingRunnable.executar(DelegatingErrorHandlingRunnable.java:54) [mola-contexto-5.2.9.RELEASE.jar!/:5.2.9.RELEASE] em java.util.concomitante.Executores$Adaptador executável.chamada(Executores.java:511) [na:1.8.0_252] em java.util.concomitante.FutureTask.runAndReset(FutureTask.java:308) [na:1.8.0_252] em java.util.concomitante.ScheduledThreadPoolExecutor$ScheduledFutureTask.acesso$301(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_252] em java.util.concomitante.ScheduledThreadPoolExecutor$ScheduledFutureTask.executar(ScheduledThreadPoolExecutor.java:294) [na:1.8.0_252] em java.util.concomitante.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_252] em java.util.concomitante.ThreadPoolExecutor$Trabalhador.executar(ThreadPoolExecutor.java:624) [na:1.8.0_252] em java.lang.Tópico.executar(Tópico.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).construir()) .recuperar() .onStatus( HttpStatus.MUITOS_PEDIDOS::iguais, resposta -> { Lista<String> cabeçalho = resposta.cabeçalhos().cabeçalho("Retry-After" (Tentar novamente depois)); Inteiro delayInSeconds; se (!cabeçalho.isEmpty()) { delayInSeconds = Inteiro.valueOf(cabeçalho.obter(0)); } mais { delayInSeconds = 60; } retorno resposta.bodyToMono(Cordas.classe).mapa(mensagem -> novo RateLimitException(mensagem, delayInSeconds)); }) .bodyToMono(Cordas.classe) |
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 |
importação java.tempo.Duração; público classe RateLimitException se estende Lançável { privado int retryAfterDelay = 60; público RateLimitException(Cordas mensagem) { super(mensagem); } público RateLimitException(Cordas mensagem, int retryAfterDelay) { super(mensagem); este.retryAfterDelay = retryAfterDelay; } público int getRetryAfterDelay() { retorno retryAfterDelay; } público Duração getRetryAfterDelayDuration() { retorno Duração.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(Cordas.classe) .retryWhen(Repetir.withThrowable(throwableFlux -> { retorno throwableFlux.filtro(t -> t instância de RateLimitException).mapa(t -> { RateLimitException rle = (RateLimitException) t; retorno Repetir.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?