Spring-Retry - シンプルで本質的なコードを汚さないリトライ処理フレームワーク

※当記事でいう'リトライ'とはバッチの世界でのジョブの再試行という意味ではなくプログラム内の特定ロジックの再試行(いわゆる retry-handler )のことをいいます

アプリケーションにおけるリトライ処理の必要性

アプリケーション構築においては、しばしばリトライ処理が必要な場面に出くわします。

例えば、

  • 外部の Web API 等の外部サービスの呼び出し時の接続失敗等を考慮してのリトライ
  • SMTP メール送信時の接続失敗等を考慮してのリトライ
  • 生成したハッシュ値を DB 格納する等の際に万が一ハッシュ値が被る可能性を考慮してのリトライ

などなど。 しかしながら、 リトライ処理を書くというのはいろいろと厄介 なものです。

リトライ処理したい部分を try - catch で囲んで〜と本質的でないコードを混ぜつつ書いていませんか?

そこで Spring-Retry

spring-projects/spring-retry

名前のとおり、 Spring Framework 環境下に限りますが Spring-Retry を使えばリトライ処理がものすごくシンプルかつ本質的なコードを汚さずに書ける のです。

しかしこの Spring-Retry 、けっこう前から存在していたプロダクトのようですが、 Spring のドキュメント一覧 にもなく、気づきにくいのです。

先日、 Spring Initializr をいじってたら選択肢にひょっこり出てきたのでそこではじめて知りました。

Spring-Retry の使い方

準備

Spring Initializr で最初から選んでおくか、既存プロジェクトであれば以下を build.gradle に書くなりします。

compile('org.springframework.retry:spring-retry')  

で、Application クラスなり @Configuration な設定なりに @EnableRetry をつけます。

@SpringBootApplication
@EnableRetry
public class Application {  
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

これで準備完了です。

素振りサンプル

Spring Boot の Controller で軽く素振りしたのでそれで説明します。

ひとまず全体のコードを記載します。

SuccessOrFailure.test() は、成功するか BarException または BazException が発生する可能性があるものとします。

@RestController
public class TestRetryController {

    /*
     * リトライ対象のメソッド
     * @Retryable アノテーションを付与
     */
    @GetMapping(path = "/retry")
    @Retryable(value = {BarException.class, BazException.class}, maxAttempts = 10, backoff = @Backoff(delay = 500))
    public String index() {
        SuccessOrFailure.test();
        return "foo";
    }

    /*
     * @Recover なメソッドは
     * - @Retryable なメソッドの最大リトライ回数分終わってもなお失敗した場合フォールバック的に呼び出される
     * - @Retryable なメソッドと同じクラスに書く必要がある
     * - @Recover なメソッドが複数ある場合は、引数の Throwable の型に一番近いメソッドが選ばれる
     * - 戻り値を @Retryable なメソッドと合わせる(この例の場合は String )必要がある
     */
    @Recover
    public String recoverBar(BarException exception) {
        return "bar";
    }

    @Recover
    public String recoverBaz(BazException exception) {
        return "baz";
    }

}

try - catch は登場しません。 リトライ処理が本質的なコード部分を汚していない のがわかると思います。

@Retryable

まず、リトライ処理したいメソッドに @Retryable アノテーションを付与します。

今回は index メソッドがそれにあたります。

@Retryable(value = {BarException.class, BazException.class}, maxAttempts = 10, backoff = @Backoff(delay = 500))

value

リトライ処理対象とする Throwable の型を指定。複数指定可能。

この例では BarException と BazException の2つを指定しています。

maxAttempts

最大リトライ試行回数を指定。

この例では 10 回にしています。

backoff

リトライ間隔を指定。@Backoff アノテーションで指定。

等間隔だけでなく、ランダムにしたりいろいろできるようです。

@Backoff アノテーションのコードを読むと設定可能な値がわかると思います。

この例では 0.5 秒間隔でやる指定にしています。

まとめると、この例では、

  • index メソッドにおいて
  • BarException または BazException が発生した場合にリトライする
  • 0.5 秒ごとに最大 10 回リトライする
  • 10 回とも失敗したら打ち切って発生した Exception を投げる

という設定になります。

@Recover

フォールバック的な処理をしたい場合、@Recover アノテーションを付与したメソッドを定義しておきます。

(フォールバック的な処理が不要な場合は用意しなくてよい)

上記サンプルコード中のコメントにも書きましたが、ここはいろいろと注意点があります。

@Recover なメソッドは、

  • @Retryable なメソッドと同じクラスに書く必要がある
  • @Recover なメソッドが複数ある場合は、引数の Throwable の型に一番近いメソッドが選ばれる
  • 戻り値を @Retryable なメソッドと合わせる(この例の場合は戻り値を String にしておく)必要がある

index メソッドは本来 "foo" を返すハズですが、10 回とも失敗した場合はこの Recover な処理によって、"bar" や "baz" が返る ようになります。


今回は説明のために Controller メソッドに適用してみましたが、実践的には、外部サービスを呼び出す Service などに適用する形がよさそうです。

Retry Template

上記では、リトライ設定をアノテーションに書きましたが、

さらに、決まりきったリトライ設定をテンプレート化して使える Retry Template という仕組みもあります。

詳しくは このあたり を読むといいでしょう。

ざっくりいうと、決まりきったリトライ設定を Retry Template として Bean 登録し、リトライしたい処理部分を Retry Template のブロックで囲む( Lambda で書ける)感じになります。

まとめ

  • 簡単にサクッと使える
  • リトライ回数等の調整値も直感的でわかりやすい
  • 本質的なコード部分を一切汚さない

Spring Framework 環境下に限る点を除いては、リトライ処理のフレームワークとしては申し分ないです。

すごくいいプロダクトだと思うので、もっともっと主張してほしいところですね。。