覚えたら書く

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

「NETFLIXの最強人事戦略 自由と責任の文化を築く」

ここ最近話題になっている「NETFLIXの最強人事戦略 自由と責任の文化を築く」を読んでみました。
話題になってるだけあって、かなり面白かったです。

NETFLIXの最強人事戦略 自由と責任の文化を築く

NETFLIXの最強人事戦略 自由と責任の文化を築く


書籍の内容紹介には以下のように書かれています

DVDの郵送レンタルから、映画のストリーミング配信、独自コンテンツ製作へと業態を進化させながら驚異的な成長を続けるNETFLIX。
その成功の秘密は、型破りな人事制度に支えられたカルチャーにある。
「業界最高水準の給料」「将来の業務にふさわしくない人は解雇」「有給休暇は廃止」等、
同社の元最高人事責任者が刺激的な戦略の精髄を示す。
「シリコンバレー史上、最も重要な文書」と呼ばれたNETFLIX CULTURE DECKを元に書籍化


紹介にも書かれている通り、Netflix(ネットフリックス)が組織として成長を続けるために行なっている人事的な戦略や現在の方針に至るまでの試行などについて書かれています。

人事に関わる人間だけではなく、全ての人が読んで価値のある本だと思います。

学ぶべき点も多いですが、Netflixという会社が高額な報酬を払えるからこそできる組織作りでもあるのかなとも思いました。
しかし逆にその強い組織作りを達成するためにハイパフォーマーな人材を招き入れてあえて高額な報酬を払っている という感じでもあります。
なかなか他社が簡単に真似できるのかというと難しい面もあります。しかし、それでもやはりその考え方や実践して成功している面は見習うべき部分があると思います。


章立ては以下のようになっています。

  • 序章: 新しい働き方
  • 第1章: 成功に貢献することが最大のモチベーション
  • 第2章: 従業員一人ひとりが事業を理解する
  • 第3章: 人はうそやごまかしを嫌う
  • 第4章: 議論を活発にする
  • 第5章: 未来の理想の会社を今からつくり始める
  • 第6章: どの仕事にも優秀な人材を配置する
  • 第7章: 会社にもたらす価値をもとに報酬を決める
  • 第8章: 円満な解雇の方法


8章のタイトルからもわかりますが、解雇方法まで人事戦略の方針に含めているというのが、
日本企業ではなかなか見られない点かなと思います。 それ以外の部分でも通常の日本企業では絶対に見られない一般原則に逆らった型破りな方針が多く見られます。

内容の詳細は、ぜひこの本を読んでもらって把握してもらいたいのですが、
本書籍内で、私が気になった部分の抜粋を一部メモしておきます。(括弧書きの部分はわかりやすくするため、私が付け足しています)

  • 20世紀に開発された複雑で面倒な人材管理手法では、21世紀の企業が直面する課題に立ち向かえるはずがない

  • 会社がどんな課題を抱えていて、どうやって対処するつもりかを、正直にかつ継続的に(全従業員に)知らせた。
    変化が常態だということ、すばやく前進するために必要とあれば、計画や人員をいくらでも変更するつもりでいることを、全員に理解してほしかった。

  • 人材管理の手法や方針を次々廃止していったのだ。破壊的変化のベースが加速するなか、製品開発の手法が時代遅れになり、無駄のない顧客中心主義の手法が必要とされるようになったのと同様、従来型のチーム構築や人材管理の手法では時代に対応できなくなっている。
    企業が人材をよりよく活かすための努力を怠っているとはいわない。そうした努力のほとんどが的外れか、逆効果だといいたいのだ。

  • ビジネスリーダーの役割は、すばらしい仕事を期限内にやり遂げる、優れたチームをつくることである。それだけ。

  • (Netflixには)企業文化を支える基本的な行動規範が存在する。

    • マネージャーは自分のチームだけではなく会社全体が取り組むべき仕事と課題を、チームメンバーにオープンにはっきりと機能的に伝える。
    • 徹底的に正直になる。同僚や上司、経営陣に対して、時期を逃さず、できれば面と向かって、ありのままを話す。
    • 事実に基づくしっかりした意見をもち、徹底的に議論し検証する。
    • 自分の正しさを証明するためではなく、顧客と会社を第一に考えて行動する。
    • 採用に関わるマネージャーは、チームが将来成功できるように、適正なスキルを備えたハイパフォーマーをすべてのポストに確実に配置する。
  • 「XをしたらYの報酬が得られる」と従業員に伝えるのは、ものごとが不変だという前提に立っている。しかし今日のビジネスに不変なものなどない。

  • ほとんどの人は、問題を見つけることが会社の最重要の仕事だと勘違いしている。「俺があの問題を見つけたんだ!」。あらそう、おめでとう。で、あなたが解決したの? 必要なのは、問題解決が好きでたまらない人材だ。

  • 過去10年間に最も成功した企業の顔ぶれを見ると、有機的に連携するチームからなる、インターネット関連企業が多い。
    「有機的に」とはどういうことか? 会社の目標や、時間と資源の配分方法、集中して取り組む問題、それらを解決する手法を、事業や顧客の必要に合わせてたえず変化させているということだ。
    そうした企業は成長し、変化し続ける有機体であって、あらかじめ決められた目標、人員、予算に縛られた、硬直的な組織ではない。

  • できる限り無駄のないプロセスと規律正しい文化をもつ企業の方が、はるかに優れた成果を出せる。それは、迅速に動けるからにほかならない。

  • 経営陣が従業員のためにできる最善のことは、一緒に働く同僚にハイパフォーマーだけを採用することだと学んだ。

  • あなたは、事業に関する問題を部下と話し合ったとき、「こいつ何もわかっちゃいないな」と思ったことがあるだろう。次にそんなことがあったら、こう考えて欲しい。「ちょっと待った、たしかにこいつは何もわかっちゃいない。でもそれは、私の知っていることを知らないからだ。教えなくては。」

  • フィードバックで最も重要なのは、「あなたはぼんやりしている」のような、相手の性格描写ではなく、行動に関するフィードバックを与えることだ。またそれは相手が改善できることではなくてはならない。
    フィードバックを受ける人が、「自分の行動の何を変えることを求められているのか」を具体的に理解できるようにする。

  • オープンで発信元が明らかなフィードバックの価値を認めている。・・・コメントの主が特定されるようになってから、フィードバックの内容はより思慮深く、建設的になった。

  • 従業員の考えていることを知りたいなら、マネージャーが直接、できれば面と向かって尋ねることに勝る方法はない。

  • 直感に基づいて行動することも多いから、データが読み取れるほどスマートで、データを無視できるほど直観力に優れた人材を探すようにしている。

  • 未来をつくるのは若者であり、彼らの知識を活用する方法を考えることは、すべてのビジネスリーダーの利益になる。

  • 将来こうあってほしいと思うチームをつくる人材を、今から採用しましょう。と(アドバイスしている)

  • ビジネスリーダーがいつも考えていなくてはならない最重要事項の一つは、「今のチームが理想のチームでないことが、私たちの足かせになっていないか?」である。

  • 「会社は家族ではなく、スポーツチームだ」という比喩を使った。優れたスポーツチームがつねに最高の選手をスカウトし、そうでない選手をラインナップから外すように、ネットフリックスのチームリーダーも継続的に人材を探し、チームを組み換えていかねばならない。

  • 必要なスキルを備えたハイパフォーマーを採用することが最善の選択肢だと思うなら、たとえそのせいで今のメンバーを解雇することになったとしても、真剣に検討してほしい

  • 今日のすべての働く人たちに私ができる最良のアドバイスは、つねに柔軟性を保ち、新しいスキルを学び、新しい機会を検討し、折あるごとに新しい課題に挑戦して、新鮮な気持ちで自分を伸ばしながら働けるようにしよう、ということだ。

  • 解雇のタイミングを判断することが、会社の求めるスキルを持つ優秀な人材を採用できるかどうかのカギを握る。二つは表裏一体の関係にある。逸材を採用する能力がなければ、いい人たちを安心して放出できない。とっちかだけうまいなんてことはあり得ないし、それじゃ優秀なチームは作れない。

  • 最も競争力のある企業は、必要な人材をいつでも積極的に採用することによって、機動性を保ち、変革と成長を続けている。
    最も優れた従業員は、やりがいのある新しい機会をつねに求め、会社に強い忠誠心をもっているか、いつか社外に機会を求めてやめていくことが多い。

  • 従業員を楽しませることは何の問題もないが、従業員と会社の双方にとってベストなのは優秀な同僚と一緒にすばらしい仕事ができるのを従業員が喜んでくれることだ

  • 仕事に対する真のゆるぎない満足感は、優れた同僚たちと真剣に問題解決に取り組むときや、懸命に生み出した製品・サービスを顧客が気に入ってくれたときにこそ得られる。

  • 新しく採用した人材が職務に適していないことが判明した場合、問題があるのはその人ではなく、採用プロセスだ。
    たんに不適当な人材を採用してしまったというだけで、その人の責任では断じてない!自分に非があると思わせてないけない。

  • 上司や同僚が指導するのもいいが、その仕事を今できる人を効率よく探すことがカギになる場合も多い。また業績不振者の能力を伸ばすことに時間をかけすぎると、彼らが他社で伸びる可能性をつぶしてしまいかねない。

  • ハイパフォーマーはすべてがうまくいってることに満足しているというよりは、むしろチームの仕事ぶりに不満を持っていることが多い。最高の成果を強く求めるからこそ、それを達成しようとする中で必然的に痛みや不満を感じるものだ。
    従業員にもってほしいのは最高を追求する姿勢であって、まじめに働きさえすれば会社が守ってくれるだろうという安易な気持ちではない。

  • 人事考課を廃止することが可能であれば、試しにやってみよう!この制度のせいで膨大な時間が無駄になっているうえ、従業員は業績に関するリアルタイム情報が得られないことが多い。


この本の内容を多くの方と共有できたらいいなーと素直に思いました。



関連リンク

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の内容を色々といじることで、リトライの方法を細かく制御できることがわかりました。



関連エントリ