この記事はSpring Advent Calendar 2022
の24日目の記事になりました。
TL;DR
-
WebClient
のリトライ処理で、頻出のサンプルはそのまま使うとリクエスト失敗以外の理由でもリトライされる場合が有る- 特に取得した
JSON
のデシリアライズに失敗した場合
- 特に取得した
- 冪等性の無い
API
に対してこの問題を踏むと、データ不整合が発生しうる -
Retry
にfilter
を付けることでこの問題は回避できる
本文
共通で利用するコードについて
以降の解説では、以下のWebClient
及びDTO
を利用します。
諸事情からKotlin
で書いていますが、Java
で書いた場合と大差有るような内容は有りません。
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.http.MediaType
import org.springframework.http.codec.json.Jackson2JsonDecoder
import org.springframework.web.reactive.function.client.WebClient
val objectMapper: ObjectMapper = jacksonObjectMapper().apply {
// 不明なプロパティは無視
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
}
val client: WebClient = WebClient.builder()
// 今回はPOSTしないのでデシリアライズ設定のみ
.codecs {
it.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON))
}
.build()
// DTOは一旦APIレスポンスの一部のみ
data class UserDto(
val login: String,
val id: Int,
val url: String
)
頻出のリトライ処理の問題点について
「Spring WebClient retry」というようにググった場合、Reactor
のリトライ機能を用いた以下のようなコードが引っかかることが多いです。
import java.time.Duration
import reactor.core.publisher.Mono
import reactor.util.retry.Retry
val userDtoMono: Mono<UserDto> = client.get()
.uri("https://api.github.com/users/k163377") // 筆者のGitHubユーザー情報の取得
.retrieve()
.bodyToMono(UserDto::class.java)
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1))) // 1秒間隔で3回リトライ
一方、この書き方では、API
通信が成功した後の処理で起きたエラーでもリトライされ、結果複数回API
が実行される危険性が有ります。
この問題は以下のように再現できます。
val userDtoMono: Mono<UserDto> = client.get()
.uri("https://api.github.com/users/k163377") // 筆者のGitHubユーザー情報の取得
.retrieve()
.bodyToMono(UserDto::class.java)
.doOnNext { println("API通信成功!$it") }
.map <UserDto> { throw Exception("何かしらのエラー!") } // API通信成功後にエラーにしてみる
.doOnError { println(it.message) }
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1))) // 1秒間隔で3回リトライ
実行結果は以下のようになります。
API
が複数回実行されてしまっていることが分かります。
API通信成功!UserDto(login=k163377, id=24751011, url=https://api.github.com/users/k163377)
何かしらのエラー!
API通信成功!UserDto(login=k163377, id=24751011, url=https://api.github.com/users/k163377)
何かしらのエラー!
API通信成功!UserDto(login=k163377, id=24751011, url=https://api.github.com/users/k163377)
何かしらのエラー!
API通信成功!UserDto(login=k163377, id=24751011, url=https://api.github.com/users/k163377)
何かしらのエラー!
例えば何かを作成するようなAPI
でこの問題が起きてしまうと、データ不整合が発生する可能性も有ります。
対処法
リトライ処理を以下のように変更し、Retry
にfilter
を付けることで、通信エラーに関してのみリトライできるようになります。
WebClientException
はWebClient
が通信エラー(ステータスが4xxまたは5xxだった)時にthrow
されるException
です。
import org.springframework.web.reactive.function.client.WebClientException
- .retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1))) // 1秒間隔で3回リトライ
+ // WebClientExceptionだった場合1秒間隔で3回リトライ
+ .retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1)).filter { it is WebClientException })