Post

Integrando retry template com rest template no Spring em Kotlin

É impossível negar que o RestTemplate ainda é muito utilizado no universo do spring, logo, ter resiliência com ele é algo necessário, mas como podemos fazer isso em Kotlin integrando com o RetryTemplate?

Um pouco de história

Antes de seguir, precisamos esclarecer uma coisa: se seu projeto é novo e ainda não usa o RestTemplate mas você esta pensando em colocá-lo, pare e não faça isso! O motivo é muito simples e pode ser encontrado nas documentações do spring.

Retirado daqui:

NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.

Ou seja, este post se aplica para clientes onde:

  • A versão utilizada do spring ainda não oferece suporte ao WebClient
  • O projeto já utiliza o RestTemplate e não há previsão de mudar para algo mais “atual”
  • Você quer fazer algo com RestTemplate mesmo sabendo que ele esta em modo de manutenção

Isto posto, o motivo de eu estar escrevendo sobre algo “em modo manutenção” esta no início do post, RestTemplate ainda é muito utilizado e eu precisei implementar o retry nele, achei justo falar sobre isso por aqui pq achei poucos conteúdos sobre o tema.

Depois pretendo escrever algo semelhante para o WebClient também, então fique ligado!

Resiliência em clientes HTTP

Eu não vou escrever exatamente sobre isso pois há muito conteúdo bom sobre o tema na internet, mas para facilitar e justificar o motivo de precisarmos adicionar estratégias a fim de garantir a resiliência de nossas chamadas HTTP, vou deixar essa talk completíssima do Rafael Ponte sobre o tema, nela todos os pontos que vamos abordar aqui são devidamente explicados e exemplificados com cenários de uso.

Configurando

O Código fonte completo desse artigo você pode encontrar aqui

Primeiro vamos precisar configurar o Spring Retry, que inicialmente era uma parte do Spring Batch mas foi transformado em um novo projeto a parte e pode ser utilizado em qualquer cenário onde você precisa que um determinado método (ou trecho de código) seja reexecutado dada uma determinada condição de erro (exception).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@EnableRetry
@Configuration
class CommonsConfiguration {

    @Bean
    fun retryTemplate(): RetryTemplate {
        return RetryTemplate.builder()
            .maxAttempts(3)
            .exponentialBackoff(50, 2.0, 3000, true)
            .retryOn(HttpServerErrorException::class.java)
            .build()
    }

    @Bean
    fun restTemplate(builder: RestTemplateBuilder): RestTemplate {
        return builder.build()
    }
}

O RetryTemplate tem mais opções para configuração e elas podem ser vistas aqui.

Dada a configuração, temos:

Na linha #1 a anotação que vai ativar o uso do RetryTemplate via annotations (não é obrigatório, pois não vou mostrar isso aqui). Nas linhas seguintes temos as configurações para que o retry possa fazer um backoff exponencial e que ele só funcione em erros de tipo http-5xx, afinal erros do tipo http-4xx não são possíveis de retry visto que representam (ou deveriam representar!) um estado inconsistente dos dados que foram enviados ao servidor [1], por isso são chamados de client errors e por fim, o builder do nosso RestTemplate.

[1] Existem dois códigos http da família 4xx que podem, sim, sofrer retry, seriam o 408 e o 429 (obrigado Rafael Ponte!) porém, no caso específico do 429 pode ser que você precise de alguma lógica de negócio antes visto que ele pode indicar que você atingiu o limite de requests por segundo/minuto/hora da API de destino

Cliente tolerante a falhas

Agora que configuramos o que precisa ser usado, vamos criar um componente para ser nosso cliente http tunado (tolerante a falhas), vou chamá-lo de EnhancedHttpClient:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
class EnhancedHttpClient(
    private val restTemplate: RestTemplate,
    private val retryTemplate: RetryTemplate
) {

    fun <T : Any> exchangeWithRetry(requestEntity: RequestEntity<*>, responseType: KClass<T>): ResponseEntity<T> {
        return retryTemplate.execute<ResponseEntity<T>, RestClientException> {
            restTemplate.exchange(
                requestEntity.url.toString(),
                requestEntity.method!!,
                requestEntity,
                responseType.java
            )
        }
    }
}

O código parece meio complexo más ele nada mais é do que uma chamada aninhada do RestTemplate dentro do RetryTemplate, assim, em caso de erros (conforme nossa configuração) o retry irá executar a quantidade de tentativas configuradas dentro do período de backoff especificado.

A complexidade maior fica por conta que dei uma “generificada” nele a fim de possibilitar o reuso por diversos clientes, isso ajuda na manutenção depois!

Testando

Agora que temos tudo configurado e pronto, podemos testar! Vamos iniciar criando uma service que irá usar nosso cliente fazendo uma chamada para uma API rest externa e que pode, eventualmente, nos retornar algum erro:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
class CatService(
    @Value("\${cat-service.url}")
    private val catServiceUrl: String,
    private val enhancedHttpClient: EnhancedHttpClient
) {

    fun findCatFact(): String {

        // O cliente http ainda poderia estar encapsulado em outro componente, isso seria util para termos
        // um maior reaproveitamento do código

        val uri = UriComponentsBuilder.fromHttpUrl(catServiceUrl).build().toUri()
        return enhancedHttpClient.exchangeWithRetry(RequestEntity("", HttpMethod.GET, uri), String::class).body!!
    }
}

Vamos escrever alguns testes:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@SpringBootTest
class ApplicationTests {

    @Value("\${cat-service.url}")
    private lateinit var catServiceUrl: String

    @Autowired
    private lateinit var restTemplate: RestTemplate

    @Autowired
    private lateinit var catService: CatService

    private lateinit var mockServer: MockRestServiceServer

    @BeforeEach
    fun setup() {
        mockServer = MockRestServiceServer.createServer(restTemplate)
    }

    @Test
    fun `should find a cat fact`() {

        mockServer.expect(once(), requestTo(catServiceUrl))
            .andExpect(method(HttpMethod.GET))
            .andRespond(
                withStatus(HttpStatus.OK)
                    .contentType(MediaType.APPLICATION_JSON)
                    .body("Some random cat fact")
            )

        val catFact = catService.findCatFact()
        assertThat(catFact).isEqualTo("Some random cat fact")

        mockServer.verify()
    }

    @Test
    fun `should retry three times before throw error`() {

        mockServer.expect(times(3), requestTo(catServiceUrl))
            .andExpect(method(HttpMethod.GET))
            .andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))

        assertThrows<HttpServerErrorException> { catService.findCatFact() }

        mockServer.verify()
    }

    @Test
    fun `should not retry when client error occur`() {

        mockServer.expect(once(), requestTo(catServiceUrl))
            .andExpect(method(HttpMethod.GET))
            .andRespond(withStatus(HttpStatus.BAD_REQUEST))

        assertThrows<HttpClientErrorException> { catService.findCatFact() }

        mockServer.verify()
    }
}

A ideia por trás dos testes é muito simples: realizamos uma chamada http para o servidor que esta mockado e quando for um caso de erro do servidor, o retry deve atuar e fazer com que o método seja chamado 3x antes de retornar a exception para a classe que chamou o método. No caso de erro do cliente, o retry não deve atuar.

Ficou com dúvidas? Comenta aqui em baixo que vou respondendo!

Esta postagem está licenciada sob CC BY 4.0 pelo autor.

Comments powered by Disqus.