第13回 MicroProfile Fault Tolerance(2) - 例で理解する非同期編

| 5 min read
Author: toshio-ogiwara toshio-ogiwaraの画像

MicroProfile Fault Tolerance(MP Fault Tolerance)を紹介する2回目は非同期呼び出しに対するフォールトトレランス処理です。今回も前回同様、MP Fault Toleranceから提供される機能とその設定を「こんなことをしたい」的な利用シーンごとに説明していきます。なお、MP Fault Toleranceの機能は豊富なため説明は前回の基本機能編、今回の非同期編、次回の設定編の3回に分けて行います。

記事はコードの抜粋を記載します。全体を見たい場合や動作を確認したい場合は以下のGitHubリポジトリを参照ください。

MicroProfileをテーマにブログを連載しています。他の記事もよければ以下のリンクからどうぞ!

Contents

Information

この記事はJava17+Helidon 3.0.1 + MicroProfile Fault Tolerance 4.0をもとに作成しています。
MicroProfile Fault Toleranceの詳細は公式マニュアルを参照くだい。

非同期機能の概要

#

基本機能編はすべて同期呼び出しの例でしたが、非同期呼び出しに対してもフォールトトレランス処理を追加することができます。

MP Fault Toleranceは@Asynchronousが付けられたメソッドの実行を別スレッドで行います。@Asynchronous@Timeout@Fallback@Bulkhead@CircuitBreaker、および@RetryのMP Fault Toleranceアノテーションと一緒に使用でき、@Asynchronousが付けられているメソッドの実行とフォールトトレランス処理は別々のスレッドで行われます。

また、メソッドは非同期で実行されるため、呼び出し元には即時に結果が返りません。このため、@AsynchronousのメソッドはFutureもしくはCompletionStageを返す必要があります。

以上の理解をもとに基本機能編で紹介した例を非同期で実行する2つの例を紹介します。なお、説明には引き続き基本機能編と同じサンプルアプリを利用します。

同時接続数を制限したい(スレッドプールスタイル)

#

バルクヘッド編で説明したメソッドの同時実行数を制限する例を非同期で実行し、同時実行数をスレッドプール数で制御するようにしてみます。

  • 呼び出す側
@GET
@Path("/16")
@Produces(MediaType.TEXT_PLAIN)
public String helloPattern16() throws Exception {
return helloService.hello_async_with_bulkhead().get();
}
  • 呼ばれる側
@Asynchronous
@Bulkhead(value = 2, waitingTaskQueue = 1)
public Future<String> hello_async_with_bulkhead() throws Exception {
var ret = helloClient.hello("longSleep");
return CompletableFuture.completedFuture(ret);
}

hello_async_with_bulkheadメソッドは@Asynchronousにより非同期で実行されますが@Bulkheadvalue属性により、このメソッドを同時に実行できるスレッド(Executorスレッド)は2つに制限されます。またhello_async_with_bulkheadメソッドが呼び出された際に空きExecutorスレッドがなければその時点でExecutionExceptionがスローされますが、waitingTaskQueueを指定することでその数だけメソッドの実行をキューイングさせることができます。

以下はキューイングやExecutionExceptionが発生する例となります。

イベント 実行状態 空き(*1) 待ち(*2)
(初期状態) - 2 0
→ 呼び出し1 実行中 1 0
→ 呼び出し2 実行中 0 0
→ 呼び出し3 実行待ち 0 1
→ 呼び出し4 ExecutionException 0 1
     呼び出し1 ← 実行完了 1 0
→ 呼び出し5 実行中 0 0
     呼び出し2 ← 実行完了 1 0

*1:空きExecutorスレッドの数。コード例の上限は2
*2:実行待ちタスク数。コード例の上限は1

タイムアウト時間を指定したい(非同期実行)

#

タイムアウト編で説明したメソッドにタイムアウト時間を指定する例と同じように今度は非同期実行するメソッドにタイムアウトを指定するようにしてみます。

  • 呼び出す側
@GET
@Path("/17")
@Produces(MediaType.TEXT_PLAIN)
public String helloPattern17() throws Exception {
return helloService.hello_async_with_timeout() // 1.
.toCompletableFuture() // 2.
.get(); // 3.
}
  • 呼ばれる側
@Asynchronous
@Timeout(500)
public CompletionStage<String> hello_async_with_timeout() throws Exception {
var ret = helloClient.hello("sleep");
return CompletableFuture.completedFuture(ret);
}

呼び出す側のhelloPattern17メソッドの流れを見ていきます。

helloPattern17メソッドは1.でhelloServiceのhello_async_with_timeoutメソッドを呼び出しますが、このメソッドは@Asynchronousにより非同期で実行されるため、処理結果の代わりにCompletionStageを受け取ります。これを2.の変換メソッドでCompletableFutureに変換した後に3.のget()hello_async_with_timeoutから結果が返されるまでhelloPattern17メソッドの実行スレッドが待ち続けます。

この際、hello_async_with_timeoutメソッドの実行時間が@Timeoutで指定された500ミリ秒よりも掛かった場合は、500ミリ秒を経過した時点で3.のgetメソッドからExecutionExceptionがスローされます。

ここで注目すべきは例外がスローされる場所です。

同期メソッドの例では必ず@Timeout@Bulkheadなどのメソッド境界から例外はスローされていましたが、今回はhello_async_with_timeoutメソッドを超えた箇所で例外がスローされています。

これはCompletionStageFutureなどの非同期結果は他の戻り値とは別に扱われるためです。MP Fault Toleranceランタイムは通常、なんらかの結果がメソッド境界を越えた時点で完了としますが@Asynchronousのメソッドから返されるCompletionStageFutureに対してはメソッドから返された時点ではなく、その非同期処理の終了をもって完了とします。

このため、@AsynchronousCompletionStageFutureが返されるメソッドでは、メソッド境界を越えたところでフォールトトレランス処理が動作します。

CompletionStageとFutureの違い

#

ここで話を少し変えて、それではMP Fault Toleranceから見てCompletionStageFutureに違いはあるのでしょうか?

答えはあります。

どちらも完了とするタイミングは上述のとおり同じですが成功と失敗の見方が異なります。CompletionStageは正常に完了したか、それとも例外的に完了したかで成功と失敗を区別しますが、Futureの場合は例外的に完了したがどうかは区別されません。よって、次のようなコードはMP Fault Toleranceランタイムに常に成功と判断されるため、意味を持ちません。

  • 意味のない例
@Asynchronous
@Retry
public Future<String> hello_async_with_retry() throws Exception {
CompletableFuture<String> future = new CompletableFuture<>();
try {
future.complete(helloClient.hello("throwRetryable"));
} catch (Exception e) {
future.completeExceptionally(e);
}
return future;
}

まとめ

#

MP Fault Toleranceは@Asynchronousを付けることで各種フォールトトレランス処理を簡単に非同期対応できるようにしてますが、本質的に非同期処理は技術的難易度が高くハマりどころが多いのも事実です。このため、実開発での利用にあたっては公式マニュアルの内容をよく理解されることをお勧めします。これは非同期に限ったことではありませんが非同期の仕組みの正しい理解は特に重要になります。

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  2. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  3. GitHub Codespaces を使いはじめる (2022-05-18)
  4. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  5. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  6. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  7. Nuxt3入門(第3回) - ユニバーサルフェッチでデータを取得する (2022-10-06)
  8. 第1回 OpenAPI Generator を使ったコード生成 (2022-06-04)
  9. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  10. Nuxt3入門(第2回) - 簡単なNuxtアプリケーションを作成する (2022-10-02)