覚えたら書く

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

FailSafeによるサーキットブレーカーパターン

クラウド、マイクロサービスなどのアーキテクチャとともにサーキットブレーカーというパターンがよく取り上げられます。

詳細は、上記リンク先などを参照していただくのがよいです。

超大雑把にいうと、クラウドのサービスなどを利用する際に、
クライアントから見てリクエスト先のサービスはネットワーク越し(インターネット越し)のリモート呼び出しになっています。

リモート呼び出しはローカルの呼び出しに比べて、色々な理由によりクライアントにエラーが返ってくる可能性が高まります。
クライアントはエラーが返ってくる可能性を織り込んでおく必要があり、リトライ(再試行)は必須となります。

が、リクエスト先のサービスが過負荷の状態に陥っていて、複数クライアントからのリクエストを上手くさばけずに、
エラーを返している状況では、クライアントからの頻繁なリトライは余計に問題を悪化させる可能性があります。

クライアントとクラウドサービスの間にブレーカーを設けるのがサーキットブレーカーパターンです。
リクエスト先から特定のエラーが複数回連続で返された場合は、一旦ブレーカーをおろして、サービスへのリクエストを遮断(クラウドサービスへリクエストを送らずして失敗させる)というような動作をします。
その後一定時間経過後に、クラウドサービスへのリクエスト経路を復活させ、リクエストを送りながら正常になったかどうかを判断して、正常と見なされれば元の状態に戻します。

と書いておいてなんですが、文章だとわかりにくいので、先ほどのリンク先などを見てもらうのが良いです。


サーキットブレーカーパターンは以下の3つのモードから成り立っています

  • Closed
    • クライアントからのリクエスト(要求)は、呼び出し先のサービスへ送られます。
    • 直近のエラーの回数が特定の閾値を超えると、Openモードに遷移します。
  • Open
    • クライアントからのリクエスト(要求)は、すぐにエラー扱いで返されます。リクエストは呼び出し先のサービスへは送られません。
    • Openモードになった時点でタイマーを開始し、タイマーの期限が切れると、Half Openモードになります
  • Half Open
    • 呼び出し先が復旧したかどうかを確認しながら別のモードに遷移させるためのものです
    • 呼び出し先が復旧した(リクエストが一定回数成功した等の)場合に、Closedモードに遷移します
    • 呼び出し先が障害状態である(リクエストに対してサービスからエラーが返された)場合に、Openモードに遷移します

これらのモードの組み合わせや遷移により、障害時に呼び出し先サービスへ余計な負荷をかけてしまう状況を避けることができます。


FailSafeが提供するサーキットブレーカー

以前のエントリでFailSafe というライブラリによるJavaでのリトライ処理(再試行処理)方法を紹介しました。


FailSafeはリトライ処理だけではなく、サーキットブレーカー用のCircuitBreakerというクラスを用意してくれています。
今回は、このCircuitBreakerを簡単に試して見ます。


呼び出される側のサービス

今回のコード例では、実際にリモート呼び出しは行わず、それの代わりに適当なクラス(以下)を用意して、
これを呼び出すことでリモート呼び出しを模倣します。

このクラスは、正常なモード と 障害モード の2パターンを持っており
正常モードでは呼び出しは成功しますが、障害モードでは呼び出しは失敗します。
また、呼び出しが行われたこと自体をロギングしており、ログから呼び出されたかどうかを知ることができます。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

public final class APIServer {

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

    private static ExecMode execMode = ExecMode.SUCCESS;

    public static Result request() throws IOException {
        logger.info("**** API server processing in progress..........[mode: {}]", execMode);

        waitTime(1, TimeUnit.SECONDS);

        if (execMode == ExecMode.FAILED) {
            throw new IOException("APIServer process failed");
        }

        return Result.OK;
    }

    public static void setExecMode(ExecMode mode) {
        execMode = mode;
    }

    public static ExecMode getExecMode() {
        return execMode;
    }

    private static void waitTime(long value, TimeUnit timeUnit) {
        try {
            Thread.sleep(timeUnit.toMillis(value));
        } catch (InterruptedException e) {
        }
    }

    enum ExecMode {
        SUCCESS,  // 正常なモード
        FAILED    // 障害モード
    }
}


FailSafeのCircuitBreaker

FailSafeCircuitBreakerは以下のように扱います

CircuitBreaker breaker = new CircuitBreaker()
        .failOn(IOException.class)  // エラーとみなす例外
        .withFailureThreshold(3)    // エラーが何回発生したらOpenモードに遷移するかの閾値
        .withSuccessThreshold(2)    // 成功が何回発生したらClosedモードに遷移するかの閾値
        .onOpen(() -> logger.warn("##### CircuitBreaker on Open"))  // Openモードに遷移したことの通知
        .onHalfOpen(() -> logger.warn("##### CircuitBreaker on Half Open"))  // Half Openモードに遷移したことの通知
        .onClose(() -> logger.warn("##### CircuitBreaker on Close"))  // Closedモードに遷移したことの通知
        .withDelay(5, TimeUnit.SECONDS);   // Closedモードに遷移してからHalf Openモードに遷移するまでの時間


このサーキットブレーカーの条件利用して実際の処理呼び出しをするには以下のようになります

Result result = Failsafe.with(breaker)
        .withFallback(Result.NG)
        .onSuccess((o, ctx) -> logger.info("onSuccess"))
        .onFailure((o, th, ctx) -> logger.error("onFailure [{}]", th.getClass().getName()))
        .get(() -> APIServer.request());


上記のCircuitBreakerの条件で実行すると以下のようになります

  • IOExceptionが返されたらエラーとみなす
  • エラーカウントが3回に達したらOpenモードに遷移する
  • Openモードに遷移したら、5秒間はそのままのモード(その間に呼び出されても即時に失敗を返す)
  • Half Openモードで、呼び出し先サービスから2回成功が返されたらClosedモードに遷移する


CircuitBreakerを利用して実行してみる

上記のサーキットブレーカーの条件でいくつか実際に処理を実行してみます。


ClosedモードからOpenモードへの遷移

サンプルコード

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

import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class BreakerSample1 {

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

    public static void main(String[] args) {
        CircuitBreaker breaker = new CircuitBreaker()
                .failOn(IOException.class)
                .withFailureThreshold(3)
                .withSuccessThreshold(2)
                .onOpen(() -> logger.warn("##### CircuitBreaker on Open"))
                .onHalfOpen(() -> logger.warn("##### CircuitBreaker on Half Open"))
                .onClose(() -> logger.warn("##### CircuitBreaker on Close"))
                .withDelay(5, TimeUnit.SECONDS);


        // 呼び出し先のAPIServerは処理が失敗するモード
        APIServer.setExecMode(APIServer.ExecMode.FAILED);

        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);

        waitTime(1, TimeUnit.SECONDS);

        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);
    }

    private static Result execWithCircuitBreaker(CircuitBreaker breaker) {
        Result result = Failsafe.with(breaker)
                .withFallback(Result.NG)
                .onSuccess((o, ctx) -> logger.info("onSuccess"))
                .onFailure((o, th, ctx) -> logger.error("onFailure [{}]", th.getClass().getName()))
                .get(() -> APIServer.request());

        logger.info("execWithCircuitBreaker [server-mode: {} -> return: {}]", APIServer.getExecMode(), result);

        return result;
    }

    private static void waitTime(long value, TimeUnit timeUnit) {

        logger.info("----------------------------------");
        logger.info("wait....[{} {}]", value, timeUnit);
        logger.info("----------------------------------");

        try {
            Thread.sleep(timeUnit.toMillis(value));
        } catch (InterruptedException e) {
        }
    }
}

上記サンプルコードですが、何をやっているかは以下の通りです

  • APIServerを障害モードにして必ずIOExceptionが返ってくるようにします。
  • 3回リクエスト送りますが全てエラー扱いになります
  • 3回エラーとなったことでOpenモードになります
  • 5秒間はOpenモードのままなので、その後の2回の呼び出しはAPIServerへのリクエスト無しでエラー扱いとなります

実行結果

21:47:27.771 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
21:47:28.780 [main] ERROR net.yyuki.breaker.BreakerSample1 - onFailure [java.io.IOException]
21:47:28.780 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
21:47:28.780 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
21:47:29.780 [main] ERROR net.yyuki.breaker.BreakerSample1 - onFailure [java.io.IOException]
21:47:29.781 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
21:47:29.781 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
21:47:30.788 [main] WARN net.yyuki.breaker.BreakerSample1 - ##### CircuitBreaker on Open
21:47:30.788 [main] ERROR net.yyuki.breaker.BreakerSample1 - onFailure [java.io.IOException]
21:47:30.788 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
21:47:30.788 [main] INFO net.yyuki.breaker.BreakerSample1 - ----------------------------------
21:47:30.788 [main] INFO net.yyuki.breaker.BreakerSample1 - wait....[1 SECONDS]
21:47:30.788 [main] INFO net.yyuki.breaker.BreakerSample1 - ----------------------------------
21:47:31.793 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
21:47:31.793 [main] INFO net.yyuki.breaker.BreakerSample1 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]


##### CircuitBreaker on Open

のログがOpenモードに遷移したことを意味しています。


最後に2行以下のようにログが出ているのは、APIServerへのリクエスト無しでエラー扱いとなっていることを意味しています。(Openモードなので)

execWithCircuitBreaker [server-mode: FAILED -> return: NG]
execWithCircuitBreaker [server-mode: FAILED -> return: NG]

APIServerへのリクエストが行われていれば以下のログが出るはずなので

**** API server processing in progress..........


OpenモードからHalf Openモードへの遷移

サンプルコード

必要な部分以外省略しています。(省略している部分は基本的にBreakerSample1と同じです)

public class BreakerSample2 {

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

    public static void main(String[] args) {
        CircuitBreaker breaker = new CircuitBreaker()
                .failOn(IOException.class)
                .withFailureThreshold(3)
                .withSuccessThreshold(2)
                .onOpen(() -> logger.warn("##### CircuitBreaker on Open"))
                .onHalfOpen(() -> logger.warn("##### CircuitBreaker on Half Open"))
                .onClose(() -> logger.warn("##### CircuitBreaker on Close"))
                .withDelay(5, TimeUnit.SECONDS);


        // 呼び出し先のAPIServerは処理が失敗するモード
        APIServer.setExecMode(APIServer.ExecMode.FAILED);

        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);

        waitTime(1, TimeUnit.SECONDS);

        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);

        waitTime(6, TimeUnit.SECONDS); // 6秒待ってHalf Openへ遷移

        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);
    }

//(省略)
}


先ほどのサンプルとの違いは、6秒待ってHalf Open モードに遷移させている部分です。
その直後の呼び出しはAPIServerへのリクエストとして扱われますが、APIServerが障害モードのままなのでエラーが返されます。
結局、再度Openモードに戻ってしまい、さらにそのあとの呼び出しはAPIServerへのリクエストまで行かずに、即座にエラーとして扱われています。


実行結果

22:00:30.208 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
22:00:31.215 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException]
22:00:31.215 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:00:31.215 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
22:00:32.217 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException]
22:00:32.217 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:00:32.218 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
22:00:33.224 [main] WARN net.yyuki.breaker.BreakerSample2 - ##### CircuitBreaker on Open
22:00:33.224 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException]
22:00:33.224 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:00:33.224 [main] INFO net.yyuki.breaker.BreakerSample2 - ----------------------------------
22:00:33.224 [main] INFO net.yyuki.breaker.BreakerSample2 - wait....[1 SECONDS]
22:00:33.224 [main] INFO net.yyuki.breaker.BreakerSample2 - ----------------------------------
22:00:34.230 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:00:34.230 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:00:34.230 [main] INFO net.yyuki.breaker.BreakerSample2 - ----------------------------------
22:00:34.230 [main] INFO net.yyuki.breaker.BreakerSample2 - wait....[6 SECONDS]
22:00:34.231 [main] INFO net.yyuki.breaker.BreakerSample2 - ----------------------------------
22:00:40.234 [main] WARN net.yyuki.breaker.BreakerSample2 - ##### CircuitBreaker on Half Open
22:00:40.234 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
22:00:41.236 [main] WARN net.yyuki.breaker.BreakerSample2 - ##### CircuitBreaker on Open
22:00:41.236 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException]
22:00:41.236 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:00:41.236 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]


以下ログが Half Open モードへ遷移したことを意味しています

##### CircuitBreaker on Half Open


最後の以下のログが、APIServerへリクエストしたがまた失敗してしまったので、Openモードに戻ってしまったことを意味しています。

22:00:40.234 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
22:00:41.236 [main] WARN net.yyuki.breaker.BreakerSample2 - ##### CircuitBreaker on Open
22:00:41.236 [main] ERROR net.yyuki.breaker.BreakerSample2 - onFailure [java.io.IOException]
22:00:41.236 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:00:41.236 [main] INFO net.yyuki.breaker.BreakerSample2 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]


Half OpenモードからClosedモードへの遷移

サンプルコード

public class BreakerSample3 {

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

    public static void main(String[] args) {
        CircuitBreaker breaker = new CircuitBreaker()
                .failOn(IOException.class)
                .withFailureThreshold(3)
                .withSuccessThreshold(2)
                .onOpen(() -> logger.warn("##### CircuitBreaker on Open"))
                .onHalfOpen(() -> logger.warn("##### CircuitBreaker on Half Open"))
                .onClose(() -> logger.warn("##### CircuitBreaker on Close"))
                .withDelay(5, TimeUnit.SECONDS);


        // 呼び出し先のAPIServerは処理が失敗するモード
        APIServer.setExecMode(APIServer.ExecMode.FAILED);

        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);

        waitTime(1, TimeUnit.SECONDS);

        execWithCircuitBreaker(breaker);

        waitTime(6, TimeUnit.SECONDS); // 6秒待ってHalf Openへ遷移

        // 呼び出し先のAPIServerは 処理がが成功するモード
        APIServer.setExecMode(APIServer.ExecMode.SUCCESS);

        execWithCircuitBreaker(breaker);
        execWithCircuitBreaker(breaker);

        waitTime(1, TimeUnit.SECONDS);

        execWithCircuitBreaker(breaker);
    }
//(省略)
}


先ほどの例との違いは、Half Openモードになった後にAPIServerが成功の結果を返すように障害モードから復旧させている点です。
これにより、その後のAPIServerへのリクエストが成功し、これが2回達成されることでClosedモードに遷移しています。


実行結果

22:06:20.965 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
22:06:21.973 [main] ERROR net.yyuki.breaker.BreakerSample3 - onFailure [java.io.IOException]
22:06:21.973 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:06:21.973 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
22:06:22.977 [main] ERROR net.yyuki.breaker.BreakerSample3 - onFailure [java.io.IOException]
22:06:22.977 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:06:22.977 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: FAILED]
22:06:23.982 [main] WARN net.yyuki.breaker.BreakerSample3 - ##### CircuitBreaker on Open
22:06:23.982 [main] ERROR net.yyuki.breaker.BreakerSample3 - onFailure [java.io.IOException]
22:06:23.982 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:06:23.982 [main] INFO net.yyuki.breaker.BreakerSample3 - ----------------------------------
22:06:23.983 [main] INFO net.yyuki.breaker.BreakerSample3 - wait....[1 SECONDS]
22:06:23.983 [main] INFO net.yyuki.breaker.BreakerSample3 - ----------------------------------
22:06:24.985 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: FAILED -> return: NG]
22:06:24.986 [main] INFO net.yyuki.breaker.BreakerSample3 - ----------------------------------
22:06:24.986 [main] INFO net.yyuki.breaker.BreakerSample3 - wait....[6 SECONDS]
22:06:24.986 [main] INFO net.yyuki.breaker.BreakerSample3 - ----------------------------------
22:06:30.988 [main] WARN net.yyuki.breaker.BreakerSample3 - ##### CircuitBreaker on Half Open
22:06:30.988 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: SUCCESS]
22:06:31.989 [main] INFO net.yyuki.breaker.BreakerSample3 - onSuccess
22:06:31.989 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: SUCCESS -> return: OK]
22:06:31.989 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: SUCCESS]
22:06:32.990 [main] WARN net.yyuki.breaker.BreakerSample3 - ##### CircuitBreaker on Close
22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - onSuccess
22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: SUCCESS -> return: OK]
22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - ----------------------------------
22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - wait....[1 SECONDS]
22:06:32.991 [main] INFO net.yyuki.breaker.BreakerSample3 - ----------------------------------
22:06:33.994 [main] INFO net.yyuki.breaker.APIServer - **** API server processing in progress..........[mode: SUCCESS]
22:06:34.996 [main] INFO net.yyuki.breaker.BreakerSample3 - onSuccess
22:06:34.997 [main] INFO net.yyuki.breaker.BreakerSample3 - execWithCircuitBreaker [server-mode: SUCCESS -> return: OK]


まとめ

簡単なサンプルではありますが、FailSafeでサーキットブレーカーパターンが扱えることがわかりました。
実際にはRetryPolicyを組み合わせたり、もっと細かな制御をすることが可能です。


関連エントリ

curlコマンドでHTTPステータスコードだけ取得

curl コマンドを実行して、レスポンスボディ等は不要で、HTTPステータスコードだけ取得したい。
というケースがあります。

pingやhealthチェックみたいな感じで、とにかく200が返ってくるのかそうじゃないのか?とか。

そんな場合は、以下の内容を実行します

curl -s {リクエスト先のURL} -o /dev/null -w '%{http_code}\n'


実行例

$curl -s http://localhost:8080/app/health -o /dev/null -w '%{http_code}\n'
200


この実行方法すぐ忘れてしまうので、自分用のメモでございました。

Failsafeによるリトライ処理 (3)

前回エントリで、FailSafeによるリトライ方法の制御をいくつか紹介しました。

今回のエントリでは、処理失敗時やリトライ処理実行時のハンドリングなどを紹介します。


基本となるリトライポリシーとリトライ処理を以下とします

リトライポリシー

RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(BusinessLogicException.class) // リトライ対象とする例外クラス
                .withDelay(2, TimeUnit.SECONDS)        // リトライ時の間隔
                .withMaxRetries(5);                    // 最大リトライ回数


リトライ処理

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

onSuccessにより、処理が成功した場合は成功時のログを出力しています。
onFailureにより、最大リトライ回数まで失敗した場合にエラーログを出力するようにしています。
(ログ出力は、slf4jなどで実行しています)


戻り値を返すようにする

これまでのエントリでは、基本的に戻り値を返さないメソッドをFailSafeでのリトライ対象としていました。
当然ですが、戻り値を返したい場合も多々あります。

この場合は、runメソッドではなくgetメソッドの中で対象の処理を実行するようにします。

例えばリトライ対象の処理が大雑把に書くと以下のような処理だとして、戻り値にResultというenumを返すものとします。

final class BusinessLogic {

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

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

            logger.info(">>BusinessLogic execute success.");
            return Result.SUCCESS;
        }
}

enum Result {
        UNKNOWN,

        FAILED,

        SUCCESS
}

この場合、FailSafeで以下のように実行します。

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

これで、対象の処理の戻り値を受け取ることができます。


リトライ最大回数まで失敗したら特定の戻り値を返す

最大回数まで処理が失敗した場合に返す戻り値を返す場合は、withFallbackで指定することが可能です。
以下例では、処理が全部失敗した場合は Result.FAILEDという値を返すようにしています。
BusinessLogic#executeは先に記載したコードのままとします)

Result ret = Failsafe.with(retryPolicy)
        .withFallback(Result.FAILED)
        .onSuccess((o, ctx) -> logger.info("bussiness logic success!!!! [execCount: {}]]", ctx.getExecutions()))
        .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))
        .get(() -> BusinessLogic.execute(time));

// 返された戻り値をログ出力してみる
logger.info("execute result -> [{}]", ret);

これを実行してみると

処理が途中で成功した場合

20:43:30.915 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493395812]
20:43:32.927 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493395812]
20:43:34.929 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493395812]
20:43:36.933 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493395812]
20:43:36.933 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic execute success.
20:43:36.933 [main] INFO net.yyuki.retry2.RetrySample2 - bussiness logic success!!!! [execCount: 4]]
20:43:36.933 [main] INFO net.yyuki.retry2.RetrySample2 - execute result -> [SUCCESS]

戻り値として Result.SUCCESSが返されています


リトライ含めて処理が全部失敗した場合

20:39:49.916 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493189830]
20:39:51.926 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493189830]
20:39:53.932 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493189830]
20:39:55.934 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493189830]
20:39:57.935 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493189830]
20:39:59.939 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536493189830]
20:39:59.942 [main] ERROR net.yyuki.retry2.RetrySample2 - bussiness logic failed.
net.yyuki.retry2.RetrySample2$BusinessLogicException: failed
    at net.yyuki.retry2.RetrySample2$BusinessLogic.execute(RetrySample2.java:51)
    at net.yyuki.retry2.RetrySample2.lambda$retryExecute$2(RetrySample2.java:38)
    at net.jodah.failsafe.SyncFailsafe.call(SyncFailsafe.java:145)
    at net.jodah.failsafe.SyncFailsafe.get(SyncFailsafe.java:56)
    at net.yyuki.retry2.RetrySample2.retryExecute(RetrySample2.java:38)
    at net.yyuki.retry2.RetrySample2.main(RetrySample2.java:24)
20:39:59.942 [main] INFO net.yyuki.retry2.RetrySample2 - execute result -> [FAILED]

戻り値として Result.FAILEDが返されています。


リトライ最大回数まで失敗したら例外をスローする

withFallback では戻り値を返すだけではなく、例外をスローすることも可能です。
例外をスローしたい場合は以下のように記述します。

Result ret = Failsafe.with(retryPolicy)
        .withFallback(() -> {throw new FaitalException("All processing failed");})
        .onSuccess((o, ctx) -> logger.info("bussiness logic success!!!! [execCount: {}]]", ctx.getExecutions()))
        .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))
        .get(() -> BusinessLogic.execute(time));


処理失敗時・リトライ時にハンドリングする

リトライする時に、その度にログを出力したいというケースがあります。
その場合、onRetry, onFailedAttemptというメソッドでハンドリング可能です。

Result ret = Failsafe.with(retryPolicy)
        .withFallback(Result.FAILED)
        .onSuccess((o, ctx) -> logger.info("bussiness logic success!!!! [execCount: {}]]", ctx.getExecutions()))
        .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))
        .onRetry((o, th, ctx) -> logger.warn("onRetry  [tryCount: {}]", ctx.getExecutions()))
        .onFailedAttempt((o, th, ctx) -> logger.warn("onFailedAttempt  [tryCount: {}]", ctx.getExecutions()))
        .get(() -> BusinessLogic.execute(time));


仮にリトライが全部失敗した場合、以下のような結果になります。

実行結果

20:57:23.457 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536494243378]
20:57:23.468 [main] WARN net.yyuki.retry2.RetrySample2 - onFailedAttempt  [tryCount: 1]
20:57:25.473 [main] WARN net.yyuki.retry2.RetrySample2 - onRetry  [tryCount: 1]
20:57:25.473 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536494243378]
20:57:25.474 [main] WARN net.yyuki.retry2.RetrySample2 - onFailedAttempt  [tryCount: 2]
20:57:27.478 [main] WARN net.yyuki.retry2.RetrySample2 - onRetry  [tryCount: 2]
20:57:27.478 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536494243378]
20:57:27.478 [main] WARN net.yyuki.retry2.RetrySample2 - onFailedAttempt  [tryCount: 3]
20:57:29.483 [main] WARN net.yyuki.retry2.RetrySample2 - onRetry  [tryCount: 3]
20:57:29.483 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536494243378]
20:57:29.483 [main] WARN net.yyuki.retry2.RetrySample2 - onFailedAttempt  [tryCount: 4]
20:57:31.488 [main] WARN net.yyuki.retry2.RetrySample2 - onRetry  [tryCount: 4]
20:57:31.488 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536494243378]
20:57:31.488 [main] WARN net.yyuki.retry2.RetrySample2 - onFailedAttempt  [tryCount: 5]
20:57:33.489 [main] WARN net.yyuki.retry2.RetrySample2 - onRetry  [tryCount: 5]
20:57:33.489 [main] INFO net.yyuki.retry2.RetrySample2 - >>BusinessLogic call execute [param: 1536494243378]
20:57:33.489 [main] WARN net.yyuki.retry2.RetrySample2 - onFailedAttempt  [tryCount: 6]
20:57:33.492 [main] ERROR net.yyuki.retry2.RetrySample2 - bussiness logic failed.
net.yyuki.retry2.RetrySample2$BusinessLogicException: failed
    at net.yyuki.retry2.RetrySample2$BusinessLogic.execute(RetrySample2.java:53)
    at net.yyuki.retry2.RetrySample2.lambda$retryExecute$4(RetrySample2.java:40)
    at net.jodah.failsafe.SyncFailsafe.call(SyncFailsafe.java:145)
    at net.jodah.failsafe.SyncFailsafe.get(SyncFailsafe.java:56)
    at net.yyuki.retry2.RetrySample2.retryExecute(RetrySample2.java:40)
    at net.yyuki.retry2.RetrySample2.main(RetrySample2.java:24)
20:57:33.492 [main] INFO net.yyuki.retry2.RetrySample2 - execute result -> [FAILED]

この結果からもわかりますが、以下の順に処理が実行されます

対象の処理実行 → (処理失敗) → onFailedAttempt → 遅延(delay) → onRetry →リトライにより対象処理実行 → (処理失敗) → onFailedAttempt → ...


onFailedAttemptが処理失敗に反応して実行されるのに対して、onRetryがリトライ処理が実行される直前に実行されます。


リトライが最大回数まで失敗した場合の通知を受け取る

リトライが最大回数を超えた場合、onRetriesExceeded で通知を受け取ることが可能です。
onFailureとの使い分けが正確によくわかっていませんが、onFailureよりも先にonRetriesExceededへの通知が行われます。

Result ret = Failsafe.with(retryPolicy)
                .withFallback(Result.FAILED)
                .onSuccess((o, ctx) -> logger.info("bussiness logic success!!!! [execCount: {}]]", ctx.getExecutions()))
                .onRetriesExceeded(ctx -> logger.warn("Failed to execute. Max retries exceeded."))
                .onFailure(throwable -> logger.error("bussiness logic failed.", throwable))
                .onRetry((o, th, ctx) -> logger.warn("onRetry  [tryCount: {}]", ctx.getExecutions()))
                .onFailedAttempt((o, th, ctx) -> logger.warn("onFailedAttempt  [tryCount: {}]", ctx.getExecutions()))
                .get(() -> BusinessLogic.execute(time));

上記の指定の場合に全部処理が失敗した場合以下のような結果になります


実行結果

21:18:36.937 [main] INFO net.yyuki.retry2.RetrySample3 - >>BusinessLogic call execute [param: 1536495516836]
21:18:36.943 [main] WARN net.yyuki.retry2.RetrySample3 - onFailedAttempt  [tryCount: 1]
21:18:38.948 [main] WARN net.yyuki.retry2.RetrySample3 - onRetry  [tryCount: 1]
21:18:38.948 [main] INFO net.yyuki.retry2.RetrySample3 - >>BusinessLogic call execute [param: 1536495516836]
21:18:38.948 [main] WARN net.yyuki.retry2.RetrySample3 - onFailedAttempt  [tryCount: 2]
21:18:40.949 [main] WARN net.yyuki.retry2.RetrySample3 - onRetry  [tryCount: 2]
21:18:40.949 [main] INFO net.yyuki.retry2.RetrySample3 - >>BusinessLogic call execute [param: 1536495516836]
21:18:40.949 [main] WARN net.yyuki.retry2.RetrySample3 - onFailedAttempt  [tryCount: 3]
21:18:42.955 [main] WARN net.yyuki.retry2.RetrySample3 - onRetry  [tryCount: 3]
21:18:42.955 [main] INFO net.yyuki.retry2.RetrySample3 - >>BusinessLogic call execute [param: 1536495516836]
21:18:42.955 [main] WARN net.yyuki.retry2.RetrySample3 - onFailedAttempt  [tryCount: 4]
21:18:42.955 [main] WARN net.yyuki.retry2.RetrySample3 - Failed to execute. Max retries exceeded.
21:18:42.957 [main] ERROR net.yyuki.retry2.RetrySample3 - bussiness logic failed.
net.yyuki.retry2.RetrySample3$BusinessLogicException: failed
    at net.yyuki.retry2.RetrySample3$BusinessLogic.execute(RetrySample3.java:54)
    at net.yyuki.retry2.RetrySample3.lambda$retryExecute$5(RetrySample3.java:41)
    at net.jodah.failsafe.SyncFailsafe.call(SyncFailsafe.java:145)
    at net.jodah.failsafe.SyncFailsafe.get(SyncFailsafe.java:56)
    at net.yyuki.retry2.RetrySample3.retryExecute(RetrySample3.java:41)
    at net.yyuki.retry2.RetrySample3.main(RetrySample3.java:24)

リトライが最大回数を超えてしまったタイミングで、onRetriesExceededへの通知が行われていることがわかります。


まとめ

処理失敗時、リトライ時の状況を細かにハンドリングできることができました。
他にもメソッドや機能が存在していますが、ぜひ試してみてください。



関連エントリ

Failsafeによるリトライ処理 (2)

 前回エントリで FailSafe によるリトライ処理について書きました。

FailSafeは前回書いた内容よりももっと色々制御できるので、それらを紹介する内容のエントリになります。
基本的には上記の前回エントリの内容がベースにあります。


前回エントリでは、基本的にRetryPolicyを以下のように指定しました。

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

これは、

  • 対象処理実行時にBusinessLogicExceptionがスローされたらリトライする
  • リトライする際に2秒のdelayを入れる
  • 最大リトライ回数は3回

という内容になります。

この内容をいじって、リトライの方法をもう少し制御します


複数の例外クラスをリトライ対象にする

BusinessLogicException以外にもFatalException がスローされる可能性があり、
そのいずれもリトライ対象としたい場合は以下のように記述できます。

RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(BusinessLogicException.class) // リトライ対象とする例外クラス1
                .retryOn(FatalException.class)         // リトライ対象とする例外クラス2
                .withDelay(2, TimeUnit.SECONDS)         // リトライ時の間隔
                .withMaxRetries(3);                     // 最大リトライ回数

または、

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

このいずれかの記述をすることで、BusinessLogicException, FatalExceptionのいずれがスローされてもリトライされます。


リトライ対象の例外クラスを instanceofでチェックする

リトライ対象とする例外クラスをinstanceofを使って判定することも可能です。
以下のように記述します。

RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(failure -> failure instanceof BusinessLogicException) // リトライ対象とする例外をinstanceofでチェック
                .withDelay(2, TimeUnit.SECONDS)         // リトライ時の間隔
                .withMaxRetries(3);                     // 最大リトライ回数


リトライ時の遅延をランダムに変化させる

リトライ時の遅延を withDelay で指定しますが、これで指定した時間きっちり遅延させた上で次の再試行が行われます。
この遅延をランダムに変化させたい場合はwithJitterメソッドを利用します。

以下のように記述します。

RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(BusinessLogicException.class)  // リトライ対象とする例外クラス
                .withDelay(2, TimeUnit.SECONDS)         // リトライ時の間隔
                .withJitter(500, TimeUnit.MILLISECONDS) // Delayをランダムに変化させる値
                .withMaxRetries(3);                     // 最大リトライ回数

上記例でいくと、2秒にランダムで-500〜+500ミリ秒 を 加算した値が遅延時間になります。


リトライのたびに遅延時間を伸ばす

リトライするたびに遅延時間を伸ばしたいというケースがあると思います。
その場合、withBackoffメソッドを利用します。

RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(BusinessLogicException.class) // リトライ対象とする例外クラス
                .withBackoff(1, 10, TimeUnit.SECONDS)  // 遅延時間は最小値1秒, 最大値10秒
                .withMaxRetries(10);                   // 最大リトライ回数

上記の例の場合は、最初の遅延時間は1秒で最大の遅延時間は10秒です。
リトライのたびに、1, 2, 4, 8, 10, 10 ・・・(秒) という遅延をするようになります。
(初期値の遅延時間を2倍していって、最大値になったら遅延時間を伸ばすのは打ち止め というような動きになります)


上記例では遅延を伸ばすための係数が自動的に2 になっているので、2倍されていく動きになっていますが、
この係数を自分で指定したい場合は、withBackoff の第4引数で指定可能です。
以下例では係数は 3 となっています。

RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(BusinessLogicException.class)    // リトライ対象とする例外クラス
                .withBackoff(1, 10, TimeUnit.SECONDS, 3)  // 遅延時間を伸ばすための係数は3
                .withMaxRetries(10);                      // 最大リトライ回数

このケースでは、リトライのたびに、1, 3, 9, 10, 10, 10 ・・・(秒) という遅延をするようになります。


実行する処理の戻り値でリトライを判定する

これまで実行対象の処理が例外をスローした場合にリトライするようにしていましたが、
対象の処理が戻り値を返す場合にその戻り値でリトライの有無を判定することも可能です。
これを実現したい場合は、retryIfメソッドを利用します。

例えば戻り値がnullの場合はリトライするとしたい場合は、以下のようになります。

RetryPolicy retryPolicy = new RetryPolicy()
                .retryIf(result -> result == null)  // 戻り値がnullの場合はリトライ
                .withDelay(2, TimeUnit.SECONDS)     // リトライ時の間隔
                .withMaxRetries(3);                 // 最大リトライ回数


または以下のようにretryWhenというメソッドでも同様のことが実現可能です

RetryPolicy retryPolicy = new RetryPolicy()
                .retryWhen(null)                 // 戻り値がnullの場合はリトライ
                .withDelay(2, TimeUnit.SECONDS)  // リトライ時の間隔
                .withMaxRetries(3);              // 最大リトライ回数


ただし、これらの戻り値での判定をするRetryPolicyを利用する場合は、 実際の処理実行においてrunメソッドで使うのでなく

Failsafe.with(retryPolicy)
        .run(() -> doSomething());


getメソッド(戻り値を取得する方法)を使う必要がありそうです。

Failsafe.with(retryPolicy)
        .get(() -> doSomething());


runメソッドだと挙動が意図しないものになるように思われます。


特定の例外がスローされたらリトライを中止する

対象の処理を実行している中で特定の例外がスローされたら場合はリトライを中止させたい、
そのような場合は、abortOnメソッドでリトライ中止対象の例外クラスを指定することで実現できます。

RetryPolicy retryPolicy = new RetryPolicy()
                .retryOn(BusinessLogicException.class) // リトライ対象とする例外クラス
                .abortOn(FatalException.class)         // この例外が発生したらリトライを中止する
                .withDelay(2, TimeUnit.SECONDS)         // リトライ時の間隔
                .withMaxRetries(3);                     // 最大リトライ回数

上記の例では、BusinessLogicExceptionはリトライ対象ですが、FatalExceptionが発生するとリトライが中止されます。


まとめ

RetryPolicyの内容を色々といじることで、リトライの方法を細かく制御できることがわかりました。



関連エントリ

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



関連エントリ