4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Resilience4j の TimeLimiter でメソッド呼び出しにタイムアウトをつける

Posted at

背景

Azure CosmosDBであったり、AWS S3であったり、FAXを送信するサービスであったり、外部のサービスを呼び出すSDKを使うシーンが多くなってきた。

これまで普通に生きてきた世界は、自分で管理するデータベースサーバーの生死だけを気にすればよくて、ネットワーク的な距離もごく近傍。すべてが想定の範囲内の出来事だったが、こうした外部のサービスを利用するときには単純にはいかない。

ネットワークの遅延は安定せず、時には秒単位での遅延も起こりうる。多くのサービスはホスト名でエンドポイントが提供され、IPアドレスは予告なく変更になる。

こうした不安定なイベントが発生したとき、各サービスをHTTPなりで呼び出してブロッキングしているスレッドが急激にたまってしまい終いにはサービス全体の停止を引き起こすようなことがある。

つまりブロッキングするような呼び出しは、関数呼び出しにタイムアウトをつけてメインのHTTPスレッドを埋めないような注意が必要だ。

Rubyならばわりと簡単に書ける。

require 'timeout'

def callAnotherWorld
    sleep 3
    return "何か外部のサービスを呼び出した結果"
end

begin
  result = Timeout.timeout(1) do # 1秒でタイムアウト
    callAnotherWorld()
  end
rescue Timeout::Error
  result = ""
end

p result                         # タイムアウトするのでブランクになる

Javaでもだいたい同じようなコードになる。

private final ExecutorService threadPool = Executors.newCachedThreadPool();

private String callAnotherWorld() {
    Future<String> f = threadPool.submit(() -> innerCallAnotherWorld());

    while (true) {
        try {
            f.get(1, TimeUnit.SECONDS);  // 1秒でタイムアウト
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        } catch (InterruptedException retry) {
        }
    }
}

private String innerCallAnotherWorld() {
    return "何か外部のサービスを呼び出した結果";
}

RubyもJavaもバックグラウンドのスレッドでメソッドが実行され、呼び出し側スレッドはその完了を待つという動作である。

Javaの場合、スレッドプールの存在と、Futureの存在が見えてしまっており、少し残念な気持ちになる。

Resilience4J

そこで Resilience4J である。

Resilience4J は最近のマイブームで、分散アーキテクチャにおけるシステムの安定性を向上させるべく、以下のパターンが簡単に利用できるようになる。

  • CircuitBreaker
  • RateLimitter
  • BulkHead
  • Retry
  • TimeLimiter

メソッド呼び出しに対するタイムアウトを実現するには、TimeLimiterを利用すればよい。
CircuitBreakerの使い方などはすぐにヒットするが、TimeLimiterについてはあまり情報がないので、自分用の備忘として書いておく。

private final ExecutorService threadPool = Executors.newCachedThreadPool();

private final TimeLimiterConfig config = TimeLimiterConfig.custom()
        .timeoutDuration(Duration.ofMillis(1000))
        .cancelRunningFuture(true)
        .build();

private String callAnotherWorld() {

    Callable<String> callable = TimeLimiter
        .decorateFutureSupplier(
            TimeLimiter.of(config),
            () -> threadPool.submit(() -> innerCallAnotherWorld())
        );

    return Try.of(callable::call)
            .getOrElse("");
}

あんまり短くならないが、catchとか例外処理がなくなり、ビジネスロジックにフォーカスできるコードになる。

Resilience4Jが提供する機能は、いまどきの分散環境において、必須となるデザインパターンで、非常に勉強になる。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?