spring-retry の RetryTemplate を使ってみた

「スタバで注文するのはいつも Venti」でおなじみの fujimura です。

とある箇所でいい感じにリトライする処理が必要になったため、spring-retryRetryTemplate を使ってみました。

準備

spring-retry を build.gradle に追加します。(version は執筆時の最新のものです。適宜読み替えるようにしてください。)

compile "org.springframework.retry:spring-retry:1.2.0.RELEASE"

実行

README.md を参考にコードを書いてみます。(直接、関係がないコードは端折っています。)

        RetryTemplate retryTemplate = new RetryTemplate();

        Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
        retryableExceptions.put(RetryableException.class, true);
        retryableExceptions.put(UnretryableException.class, false);
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5, retryableExceptions);
        retryTemplate.setRetryPolicy(retryPolicy);

        ExponentialBackOffPolicy exponentialBackOffPolicy = new ExponentialBackOffPolicy();
        exponentialBackOffPolicy.setInitialInterval(500);
        exponentialBackOffPolicy.setMultiplier(2);
        retryTemplate.setBackOffPolicy(exponentialBackOffPolicy);

RetryTemplate のインスタンスに必要な情報を追加していきます。追加する情報は RetryPolicyBackOffPolicy です。

RetryPolicy は再試行のルールを決めるものです。今回は規定回数を指定する SimpleRetryPolicy を指定してみます。 SimpleRetryPolicy にはどの例外が投げられた時に再試行 or 断念するかを決めるマップを指定することができます。 今回は確認のために再試行できる例外 (RetryableException) と断念する例外 (UnretryableException) を登録してみます。

BackOffPolicy は再試行間隔を決めるものです。今回は間隔を徐々に変化させていく ExponentialBackOffPolicy を指定してみます。 初期値 (initialInterval) は 500ms に、係数 (Multiplier) は 2 にすることで倍々にしていくことにします。

RetryTemplate の設定ができたため、これを使っていろいろなパターンでの再試行をやってみます。

        log.info("Success");
        int resultSuccess = retryTemplate.execute(new RetryCallback<Integer, Throwable>() {

            @Override
            public Integer doWithRetry(RetryContext context) throws Throwable {
                return 42;
            }
        });
        log.info("  Result = {}", resultSuccess);

        log.info("Success After Failure");
        int resultSuccessAfterFailure = retryTemplate.execute(new RetryCallback<Integer, RetryableException>() {
            FrequencyRestriction frequencyRestriction = new FrequencyRestriction(3);

            @Override
            public Integer doWithRetry(RetryContext context) throws RetryableException {

                if (!frequencyRestriction.isLimited()) {
                    log.info("  trial = {}", frequencyRestriction.getCurrent());
                    throw new RetryableException();
                }

                return 42;
            }
        });
        log.info("  Result = {}", resultSuccessAfterFailure);

        log.info("Failure");
        try {
            retryTemplate.execute(new RetryCallback<Integer, RetryableException>() {

                private int current = 0;

                @Override
                public Integer doWithRetry(RetryContext context) throws RetryableException {
                    log.info("  Trial = {}", ++current);
                    throw new RetryableException();
                }
            });
        } catch (RetryableException e) {
            log.info("  Failure");
        }

        log.info("Failure Immediately");
        try {
            retryTemplate.execute(new RetryCallback<Integer, UnretryableException>() {

                @Override
                public Integer doWithRetry(RetryContext context) throws UnretryableException {
                    log.info("  Trial = {}", 1);
                    throw new UnretryableException();
                }
            });
        } catch (UnretryableException e) {
            log.info("  Failure");
        }

        log.info("Recovery");
        int resultRecovery = retryTemplate.execute(new RetryCallback<Integer, UnretryableException>() {

            @Override
            public Integer doWithRetry(RetryContext context) throws UnretryableException {
                log.info("  trial = {}", 1);
                throw new UnretryableException();
            }
        }, new RecoveryCallback<Integer>() {
            @Override
            public Integer recover(RetryContext context) throws Exception {
                return 42;
            }
        });
        log.info("  Result = {}", resultRecovery);

今回は 5 パターンを実装してみました。

  1. (初回で) 成功するパターン
  2. (規定回数以内の失敗を経て) 成功するパターン
  3. 規定回数以内に成功できずに失敗するパターン
  4. 再試行を断念する例外の発生により規定回数を満たさずに失敗するパターン
  5. 失敗したがリカバリするパターン

さっそく実行結果を見てみます。

2017-01-14 15:36:49:862 +0900 [main] INFO Main - Success
2017-01-14 15:36:49:882 +0900 [main] INFO Main -   Result = 42
2017-01-14 15:36:49:883 +0900 [main] INFO Main - Success After Failure
2017-01-14 15:36:49:886 +0900 [main] INFO Main -   trial = 1
2017-01-14 15:36:50:390 +0900 [main] INFO Main -   trial = 2
2017-01-14 15:36:51:394 +0900 [main] INFO Main -   trial = 3
2017-01-14 15:36:53:398 +0900 [main] INFO Main -   Result = 42
2017-01-14 15:36:53:399 +0900 [main] INFO Main - Failure
2017-01-14 15:36:53:401 +0900 [main] INFO Main -   Trial = 1
2017-01-14 15:36:53:904 +0900 [main] INFO Main -   Trial = 2
2017-01-14 15:36:54:908 +0900 [main] INFO Main -   Trial = 3
2017-01-14 15:36:56:912 +0900 [main] INFO Main -   Trial = 4
2017-01-14 15:37:00:917 +0900 [main] INFO Main -   Trial = 5
2017-01-14 15:37:00:917 +0900 [main] INFO Main -   Failure
2017-01-14 15:37:00:918 +0900 [main] INFO Main - Failure Immediately
2017-01-14 15:37:00:919 +0900 [main] INFO Main -   Trial = 1
2017-01-14 15:37:00:920 +0900 [main] INFO Main -   Failure
2017-01-14 15:37:00:920 +0900 [main] INFO Main - Recovery
2017-01-14 15:37:00:923 +0900 [main] INFO Main -   trial = 1
2017-01-14 15:37:00:923 +0900 [main] INFO Main -   Result = 42

1 のパターンはそのままですね。

2 のパターンは 4 回目で成功するようなカウンタを実装してます。これにより 3 回目までは再試行可能とされる例外を投げて return まで到達しないことで再試行がされています。

3 のパターンは最終的に再試行可能を意味する例外がそのまま RetryTemplate#execute の外にまで出てきています。 ちなみに試行間隔ですが、ログのタイムスタンプを見ると 500ms → 1s → 2s → 4s というように BackOffPolicy に指定した初期間隔と係数で設定されていることがわかります。

4 のパターンは再試行不可とされる例外を投げるため、再試行されずにすぐに RetryTemplate#execute の外にまで出てきています。

5 のパターンは 4 と同じですが、リカバリ処理を指定しているため、そちらが呼ばれていることがわかります。

まとめ

spring-retry の RetryTemplate を使って、再試行する処理を比較的簡単に書けることがわかりました。

また、今回使用したサンプルは github に置いてありますので、ご参考ください。

$ git clone https://github.com/fd00/spring-retrytemplate-samples.git
$ cd spring-retrytemplate-samples
$ ./gradlew run