🍃

WebClientでリトライ処理

2022/02/17に公開

やりたいこと

  • WebClientのリトライ処理のサンプルコードを書いて動きを確かめてみる
  • リトライ処理は通常のリトライに加えて、冗長構成をとっているAPIに対して、片方のAPIが失敗したら、別のAPIを叩くようなことも実験してみる

準備

@RestController
public class WebClientController {

    private final WebClient webClient;

    @Autowired
    public WebClientController(WebClient.Builder builder) {
        webClient = builder.baseUrl("http://localhost:8080").build();
    }

    private Mono<String> get(String uri) {
        return webClient.get()
                .uri(uri)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(String.class);
    }
}

このコントローラにいろいろなエンドポイントを追加してみる。

動かしてみる

通常

まずは普通に動いているAPIを呼んでみる。

    @GetMapping("ok")
    public ResponseEntity<String> getOK() {
        return new ResponseEntity<>("OK", new HttpHeaders(), HttpStatus.OK);
    }

    @GetMapping
    public Mono<String> get() {
        return get("ok");
    }

結果:

% curl -i http://localhost:8080/ 
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 2
Date: Thu, 17 Feb 2022 04:20:01 GMT

OK

問題なく結果がとれることを確認できました。

同一URLに対してリトライ

一定の確率で失敗するAPIを用意します。

    @GetMapping("randError")
    public ResponseEntity<String> getRandError() {
        if (Math.random() > 0.5) {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }
        return new ResponseEntity<>("OK", new HttpHeaders(), HttpStatus.OK);
    }

    @GetMapping
    public Mono<String> get() {
        return get("randError");
    }
% curl -i http://localhost:8080/
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 2
Date: Thu, 17 Feb 2022 04:29:32 GMT

OK
% curl -i http://localhost:8080/
HTTP/1.1 400 
Content-Length: 0
Date: Thu, 17 Feb 2022 04:29:34 GMT
Connection: close

試行回数を増やすのを実現するのは割と簡単でretry()を付けるだけ。

    @GetMapping
    public Mono<String> get() {
        return get("randError").retry(10);
    }

リトライに間隔を開けたいときはこういうことも(例は、失敗後2秒間隔で10回リトライする)

    @GetMapping
    public Mono<String> get() {
        return get("randError").retryWhen(Retry.fixedDelay(10, Duration.ofSeconds(2)));
    }

セカンダリ(別URL)に対してリトライ

APIのエンドポイントが複数用意されている場合、リトライは別のURLを叩きたいというケースもあると思います。例では/primary(必ず失敗)と/secondary(必ず成功)を用意し、/primaryを叩いたあとに/secondaryを叩くみたいなことを実験してみる。

    @GetMapping("/primary")
    public ResponseEntity<String> getError() {
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
    
    @GetMapping("/secondary")
    public ResponseEntity<String> getOK() {
        return new ResponseEntity<>("OK", new HttpHeaders(), HttpStatus.OK);
    }
    
    @GetMapping
    public Mono<String> get() {
        return get("primary").onErrorResume(t -> get("secondary"));
    }

何事もなくOKが返ってきました。

% curl -i http://localhost:8080/
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 2
Date: Thu, 17 Feb 2022 04:55:53 GMT

OK

プライマリをリトライしたあとに、セカンダリを叩く

先程の応用でOK。ログを埋め込んで動かしてみます。

    private Mono<String> get(String uri) {
        return webClient.get()
                .uri(uri)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(String.class)
                .retry(5); // ここにリトライを指定することで各エンドポイントにリトライ処理が走る
    }

    @GetMapping("primary")
    public ResponseEntity<String> getError() {
        log.info("primary failed");
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }

    @GetMapping("secondary")
    public ResponseEntity<String> getRandError() {
        if (Math.random() > 0.3) {
            log.info("secondary failed");
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }
        return new ResponseEntity<>("OK", new HttpHeaders(), HttpStatus.OK);
    }

    @GetMapping
    public Mono<String> get() {
        return get("primary").onErrorResume(t -> get("secondary"));
    }

アクセス失敗ログが出ているけど、最終的にOKが返ってくることを確認できました。

% curl -i http://localhost:8080/
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 2
Date: Thu, 17 Feb 2022 06:55:26 GMT

OK
2022-02-17 15:55:16.427  INFO 79271 --- [nio-8080-exec-1] WebClientController        : primary failed
2022-02-17 15:55:17.434  INFO 79271 --- [nio-8080-exec-2] WebClientController        : primary failed
2022-02-17 15:55:18.444  INFO 79271 --- [nio-8080-exec-3] WebClientController        : primary failed
2022-02-17 15:55:19.459  INFO 79271 --- [nio-8080-exec-4] WebClientController        : primary failed
2022-02-17 15:55:20.473  INFO 79271 --- [nio-8080-exec-5] WebClientController        : primary failed
2022-02-17 15:55:21.487  INFO 79271 --- [nio-8080-exec-6] WebClientController        : primary failed
2022-02-17 15:55:22.499  INFO 79271 --- [nio-8080-exec-7] WebClientController        : secondary failed
2022-02-17 15:55:23.509  INFO 79271 --- [nio-8080-exec-8] WebClientController        : secondary failed
2022-02-17 15:55:24.519  INFO 79271 --- [nio-8080-exec-9] WebClientController        : secondary failed
2022-02-17 15:55:25.528  INFO 79271 --- [io-8080-exec-10] WebClientController        : secondary failed

参考

Discussion