覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Failsafeによるリトライ処理

業務系プログラムにおいて、リトライ処理は必須だと思いますが、
Javaで単純にリトライ処理を書こうとすると、泥臭い感じのプログラムになってしまいます。

FailSafeというライブラリを利用することで、リトライ処理を整理した形で記述することが可能です。

FailSafeは、リトライ以外のエラーハンドリング処理も提供しています)


リトライ処理のよくある要件

リトライ処理(再試行時)の要件としてあがってくることがある内容に以下のようなものがあります。

  • リトライ対象とするエラーや失敗条件を指定したい
  • リトライ回数を指定したい
  • リトライ時にディレイをかけたい(遅延させたい)
  • リトライ時の遅延時間をリトライのたびに伸ばしたい
  • リトライ時の遅延時間を一定範囲でランダムな値にしたい(ゆらぎを入れたい)
  • リトライ中であることをハンドリングしたい(例えばログに出したい)
  • 処理成功と失敗で処理分岐したい
  • リトライ処理が全て失敗した場合の戻り値を指定したい
  • リトライ処理の対象から除外するエラー条件を指定したい

etc...

このエントリで全てを紹介するわけではありませんが、FailSafeは上記の内容などを綺麗に扱ってくれます。


利用準備

Mavenの場合、pom.xml に以下の依存関係を追記してください。(今回はバージョン 1.1.0 を使用しました)

<dependency>
    <groupId>net.jodah</groupId>
    <artifactId>failsafe</artifactId>
    <version>1.1.0</version>
</dependency>


リトライのサンプル

FailSafeとは直接の関係性はありませんが、今回のサンプルコードで各所でロギング処理を行うため
以下の依存関係も追加しています

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>


基本的な使い方

基本的に、 net.jodah.failsafe.RetryPolicy でその名の通りリトライのポリシーを定めて、
net.jodah.failsafe.Failsafe で、そのポリシーとともに対象の処理を実行する。という流れになります。

いろいろ省いていますが、おおよそ以下のようなコードのイメージになります

import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;

// リトライのポリシーを定める (特定の例外がスローされたら、2秒間隔で最大3回リトライする)
RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(BusinessLogicException.class)  // リトライ対象とする例外クラス
                .withDelay(2, TimeUnit.SECONDS)         // リトライ時の間隔
                .withMaxRetries(3);                     // 最大リトライ回数

// 定めたリトライポリシーとともに対象の処理を実行する
Failsafe.with(retryPolicy)
                .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))  // リトライ含めて全部失敗した時の処理
                .run(() -> BusinessLogic.failedExecute(time));                               // 実行したい対象の処理


例えば以下のような実行サンプルがあったとします。
BusinessLogic#failedExecuteが実行したい処理で、BusinessLogicExceptionがスローされたらリトライするようにしています。

■サンプルコード

import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

public class RetrySample1 {

    private static final Logger logger = LoggerFactory.getLogger(RetrySample1.class);

    public static void main(String[] args) {
        RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(BusinessLogicException.class) // リトライ対象とする例外クラス
                .withDelay(2, TimeUnit.SECONDS) // リトライ時の間隔
                .withMaxRetries(3);                   // 最大リトライ回数


        final long time = System.currentTimeMillis();

        try {
            retryExecute(time, retryPolicy);
        } catch (Exception e) {
            logger.error("retry over.", e);
        }


        logger.info("--------------- end ------------------");
    }

    private static void retryExecute(long time, RetryPolicy retryPolicy) {
        Failsafe.with(retryPolicy)
                .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))
                .run(() -> BusinessLogic.failedExecute(time));
    }


    static final class BusinessLogic {

        static void failedExecute(long time) {
            logger.info(">>BusinessLogic call failedExecute [param: {}]", time);

            if ((System.currentTimeMillis() - time) < 3000) {
                throw new BusinessLogicException("failed");
            }

            logger.info(">>BusinessLogic failedExecute success.");
        }
    }

    static final class BusinessLogicException extends RuntimeException {

        public BusinessLogicException(String message) {
            super(message);
        }
    }

}

実行結果は以下の通りです

■実行結果

01:50:45.820 [main] INFO net.yyuki.retry.RetrySample1 - >>BusinessLogic call failedExecute [param: 1536339045746]
01:50:47.826 [main] INFO net.yyuki.retry.RetrySample1 - >>BusinessLogic call failedExecute [param: 1536339045746]
01:50:49.831 [main] INFO net.yyuki.retry.RetrySample1 - >>BusinessLogic call failedExecute [param: 1536339045746]
01:50:49.831 [main] INFO net.yyuki.retry.RetrySample1 - >>BusinessLogic failedExecute success.
01:50:49.831 [main] INFO net.yyuki.retry.RetrySample1 - --------------- end ------------------

わかりにくいですがリトライ2回目で、対象の処理の実行に成功しています


例えばBusinessLogic#failedExecuteの中身をいじって、何度リトライしても失敗するようにします。

■サンプルコード

import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

public class RetrySample1 {

    private static final Logger logger = LoggerFactory.getLogger(RetrySample1.class);

    public static void main(String[] args) {
        RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(BusinessLogicException.class)
                .withDelay(2, TimeUnit.SECONDS)
                .withMaxRetries(3); 


        long time = System.currentTimeMillis();

        try {
            retryExecute(time, retryPolicy);
        } catch (Exception e) {
            logger.error("retry over.", e);
        }

        logger.info("--------------- end ------------------");
    }

    private static void retryExecute(long time, RetryPolicy retryPolicy) {
        Failsafe.with(retryPolicy)
                .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))
                .run(() -> BusinessLogic.failedExecute(time));
    }

    static final class BusinessLogic {

        static void failedExecute(long time) {
            logger.info(">>BusinessLogic call failedExecute [param: {}]", time);

            if ((System.currentTimeMillis() - time) >= 0) {
                throw new BusinessLogicException("failed");
            }

            logger.info(">>BusinessLogic failedExecute success.");
        }
    }

    static final class BusinessLogicException extends RuntimeException {

        public BusinessLogicException(String message) {
            super(message);
        }
    }

}

そうした場合の実行結果は以下の通りです。

■実行結果

01:54:43.116 [main] INFO net.yyuki.retry.RetrySample2 - >>BusinessLogic call failedExecute [param: 1536339283020]
01:54:45.125 [main] INFO net.yyuki.retry.RetrySample2 - >>BusinessLogic call failedExecute [param: 1536339283020]
01:54:47.129 [main] INFO net.yyuki.retry.RetrySample2 - >>BusinessLogic call failedExecute [param: 1536339283020]
01:54:49.133 [main] INFO net.yyuki.retry.RetrySample2 - >>BusinessLogic call failedExecute [param: 1536339283020]
01:54:49.137 [main] ERROR net.yyuki.retry.RetrySample2 - bussiness logic failed.
net.yyuki.retry.RetrySample2$BusinessLogicException: failed
    at net.yyuki.retry.RetrySample2$BusinessLogic.failedExecute(RetrySample2.java:47)
    at net.yyuki.retry.RetrySample2.lambda$retryExecute$1(RetrySample2.java:37)
    at net.jodah.failsafe.Functions$10.call(Functions.java:252)
    at net.jodah.failsafe.SyncFailsafe.call(SyncFailsafe.java:145)
    at net.jodah.failsafe.SyncFailsafe.run(SyncFailsafe.java:81)
    at net.yyuki.retry.RetrySample2.retryExecute(RetrySample2.java:37)
    at net.yyuki.retry.RetrySample2.main(RetrySample2.java:24)
01:54:49.137 [main] ERROR net.yyuki.retry.RetrySample2 - retry over.
net.yyuki.retry.RetrySample2$BusinessLogicException: failed
    at net.yyuki.retry.RetrySample2$BusinessLogic.failedExecute(RetrySample2.java:47)
    at net.yyuki.retry.RetrySample2.lambda$retryExecute$1(RetrySample2.java:37)
    at net.jodah.failsafe.Functions$10.call(Functions.java:252)
    at net.jodah.failsafe.SyncFailsafe.call(SyncFailsafe.java:145)
    at net.jodah.failsafe.SyncFailsafe.run(SyncFailsafe.java:81)
    at net.yyuki.retry.RetrySample2.retryExecute(RetrySample2.java:37)
    at net.yyuki.retry.RetrySample2.main(RetrySample2.java:24)
01:54:49.137 [main] INFO net.yyuki.retry.RetrySample2 - --------------- end ------------------


この結果内容からコード中の

Failsafe.with(retryPolicy)
                .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))
                .run(() -> BusinessLogic.failedExecute(time));

onFailure で記述した処理が実行されていることがわかります。
それと同時に 上記の FailSafeで記述したリトライ処理の呼び出し元に BusinessLogicException がスローされていることもわかります。("retry over." というログのあたりがそれを表しています)


成功した場合に処理を実行する

処理が成功した場合に、何か処理を実行する場合は onSuccess で処理を記述します。
(ここではINFOレベルでsuccessという文字列と実行何回目だったかを出力しています)

Failsafe.with(retryPolicy)
             .onSuccess((o, ctx) -> logger.info("bussiness logic success!!!! [execCount: {}]]", ctx.getExecutions()))
             .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))
             .run(() -> BusinessLogic.failedExecute(time));


こうした場合実行結果は以下のようになります

■実行結果

01:57:51.922 [main] INFO net.yyuki.retry.RetrySample3 - >>BusinessLogic call failedExecute [param: 1536339471848]
01:57:53.930 [main] INFO net.yyuki.retry.RetrySample3 - >>BusinessLogic call failedExecute [param: 1536339471848]
01:57:55.936 [main] INFO net.yyuki.retry.RetrySample3 - >>BusinessLogic call failedExecute [param: 1536339471848]
01:57:55.936 [main] INFO net.yyuki.retry.RetrySample3 - >>BusinessLogic failedExecute success.
01:57:55.936 [main] INFO net.yyuki.retry.RetrySample3 - bussiness logic success!!!! [execCount: 3]]
01:57:55.936 [main] INFO net.yyuki.retry.RetrySample3 - --------------- end ------------------

実行3回目で成功していることがわかります。


最大回数までリトライ失敗したら対象の例外はスローしない

以下のように記述すると最大回数までリトライが失敗した場合に、対象の例外を呼び出し元にスローしなくなります

Failsafe.with(retryPolicy)
                .withFallback(() -> {})
                .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))
                .run(() -> BusinessLogic.failedExecute(time));

こうした場合実行結果は以下のようになります


■実行結果

02:00:04.368 [main] INFO net.yyuki.retry.RetrySample4 - >>BusinessLogic call failedExecute [param: 1536339604273]
02:00:06.377 [main] INFO net.yyuki.retry.RetrySample4 - >>BusinessLogic call failedExecute [param: 1536339604273]
02:00:08.380 [main] INFO net.yyuki.retry.RetrySample4 - >>BusinessLogic call failedExecute [param: 1536339604273]
02:00:10.385 [main] INFO net.yyuki.retry.RetrySample4 - >>BusinessLogic call failedExecute [param: 1536339604273]
02:00:10.388 [main] ERROR net.yyuki.retry.RetrySample4 - bussiness logic failed.
net.yyuki.retry.RetrySample4$BusinessLogicException: failed
    at net.yyuki.retry.RetrySample4$BusinessLogic.failedExecute(RetrySample4.java:47)
    at net.yyuki.retry.RetrySample4.lambda$retryExecute$2(RetrySample4.java:37)
    at net.jodah.failsafe.Functions$10.call(Functions.java:252)
    at net.jodah.failsafe.SyncFailsafe.call(SyncFailsafe.java:145)
    at net.jodah.failsafe.SyncFailsafe.run(SyncFailsafe.java:81)
    at net.yyuki.retry.RetrySample4.retryExecute(RetrySample4.java:37)
    at net.yyuki.retry.RetrySample4.main(RetrySample4.java:24)
02:00:10.389 [main] INFO net.yyuki.retry.RetrySample4 - --------------- end ------------------

最大回数リトライ処理を行って処理が失敗した場合でも、呼び出し元への対象例外のスローが行われなくなっています。


まとめ

FailSafe を使うことでリトライ処理をシンプルに記述できました。
今回の例では、戻り値を返さないメソッドの呼び出しのみしか試していませんが、戻り値を返すメソッドを扱うことも可能です。
また、もっと複雑な制御を行うことも可能です。(その辺はまた別の機会に・・・)



関連エントリ