覚えたら書く

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

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への通知が行われていることがわかります。


まとめ

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



関連エントリ