複製コンテキスト、コンテキストローカル、非同期処理と伝播
伝統的な、ブロッキング、同期フレームワークを使う場合、各リクエストの処理は専用のスレッドで実行されます。
つまり、処理全体に対して同じスレッドが使われます。
このスレッドは、処理が完了するまで他の実行に使われることはありません。
セキュリティプリンシパルやトレース ID のような、処理に沿ったデータの伝搬が必要な場合は、 ThreadLocals
を使うことができます。
伝搬されたデータは、処理が完了するとクリアされます。
リアクティブ実行モデルと非同期実行モデルを使用する場合、同じメカニズムを使用することはできません。
多くのプロセススレッドを使用せず、リソースの使用量を減らす(アプリケーションの並行性を高める)ために、同じスレッドを使用して複数の並行処理を処理することができます。
したがって、 ThreadLocals
を使用することはできません。さまざまな並行処理の間で値が漏れてしまうからです。
Vert.xの 複製コンテキスト は、同じような伝搬を提供する構造体ですが、非同期処理用です。同期コードでも使用できます。
このドキュメントでは、複製コンテキストについて、その取得方法、使用方法、(非同期)処理での伝搬方法を説明します。
リアクティブ・モデル
このセクションはリアクティブモデルの説明ではありません。詳細については、 Quarkusリアクティブアーキテクチャ を参照してください。 |
Quarkusはリアクティブエンジンを採用しています。 このエンジンは、最新のコンテナ化されたクラウドネイティブなアプリケーションに対応する効率性と並行性を提供します。
たとえば、Quarkus REST(旧RESTEasy Reactive)やgRPCを使用すると、QuarkusはI/Oスレッドでビジネスロジックを呼び出すことができます。 これらのスレッドは イベントループ と名付けられ、 マルチリアクターパターン を実装します。
命令型モデルを使用する場合、Quarkusは各処理ユニット(HTTPリクエストやgRPC呼び出しなど)にワーカースレッドを関連付けます。 このスレッドは、処理が完了するまで、この特定の処理に専念します。 したがって、 スレッドローカル を使用して、処理に沿ってデータを伝搬することができます。 現在の処理が完了するまで、他の処理ユニットがそのスレッドを使用することはありません。
リアクティブ・モデルでは、コードはイベントループのスレッドで実行されます。 これらのイベントループは、複数の同時処理ユニットを実行します。 例えば、同じイベントループで複数のHTTPリクエストを同時に処理することができます。 次の図は、このリアクティブ実行モデルを示しています:

これらのイベントループは 絶対に ブロックしてはいけません。 もしブロックしてしまうと、モデル全体が崩壊してしまいます。 このように、HTTPリクエストの処理がI/O操作(外部サービスの呼び出しなど)を実行する必要がある場合、その処理は:
-
操作をスケジュールします、
-
継続(I/O が完了したときに呼び出すコード)を渡します、
-
スレッドを解放します。
そのスレッドは別の同時リクエストを処理できます。 スケジュールされた操作が完了すると、渡された継続を 同じイベントループで 実行します。
このモデルは特に効率的で(スレッド数が少なく)、パフォーマンスも高い(コンテキストスイッチの回避)です。 しかし、異なる開発モデルが必要で、 スレッドローカルを 使うことはできません。 実際、これらはすべて同じスレッド、つまりイベントループで処理されます。
MicroProfile Context Propagation 仕様はこの問題に対応しています。 別の処理ユニットに切り替えるたびに、スレッドローカルに保存された値を保存して復元します。 しかし、このモデルは高価です。 コンテキスト・ローカル( 重複コンテキスト とも呼ばれる)は、これを行う別の方法であり、必要な仕組みが少なくて済みます。
コンテキストと重複コンテキスト
Quarkusでは、リアクティブコードを実行すると、実行スレッド(イベントループまたはワーカースレッド)を表す コンテキスト で実行されます。
@GET
@NonBlocking // Force the usage of the event loop
@Path("/hello1")
public String hello1() {
Context context = Vertx.currentContext();
return "Hello, you are running on context: %s and on thread %s".formatted(context, Thread.currentThread()); (1)
}
@GET
@Path("/hello2")
public String hello2() { // Called on a worker thread (because it has a blocking signature)
Context context = Vertx.currentContext();
return "Hello, you are running on context: %s and on thread %s".formatted(context, Thread.currentThread()); (2)
}
1 | 生成: Hello 1, you are running on context: io.vertx.core.impl.DuplicatedContext@5dc42d4f and on thread Thread[vert.x-eventloop-thread-1,5,main] - イベントループで呼び出されます。 |
2 | 生成: Hello 2, you are running on context: io.vertx.core.impl.DuplicatedContext@41781ccb and on thread Thread[executor-thread-1,5,main] - ワーカースレッドで呼び出されます。 |
この Context オブジェクトを使用すると、同じコンテキスト内で操作をスケジュールできます。 上の図で説明したように、コンテキストは (イベントループにアタッチされるため)、同じイベントループで continuation を実行する場合に便利です。 たとえば何かを非同期的に呼び出す必要がある場合は、現在のコンテキストをキャプチャーし、レスポンスが到着すると、そのコンテキストで continuation を呼び出します。
public Uni<String> invoke() {
Context context = Vertx.currentContext();
return invokeRemoteService()
.emitOn(runnable -> context.runOnContext(runnable)); (1)
}
1 | 同じコンテキストで結果を出力します。 |
ほとんどのQuarkusクライアントは自動的にこれを行い、適切なコンテキストで継続を呼び出します。 |
コンテクストには2つのレベルがあります:
-
イベントループを表すルートコンテキストは、同時処理間でデータを漏らさずに伝播するために使用することはできません。
-
複製コンテキストは、ルートコンテキストに基づきますが、共有されず、処理ユニットを表します。
このように、複製コンテキストは各処理ユニットに関連付けられています。 複製コンテキストは、依然としてルートコンテキストに関連付けられており、複製コンテキストを使用するスケジューリング操作は、関連付けられたルートコンテキストで実行されます。 しかし、ルートコンテキストとは異なり、処理ユニット間で共有されることはありません。 しかし、ある処理ユニットの継続は、同じ複製コンテキストを使用します。そのため、先ほどのコードスニペットでは、同じイベントループだけでなく、同じ複製コンテキストで呼び出されます(キャプチャされたコンテキストが複製コンテキストであると仮定します。)

コンテキスト・ローカルデータ
複製コンテキストで実行された場合、コードは他の並行処理と共有することなくデータを保存できます。 そのため、ローカルデータの保存、取得、削除を行うことができます。同じ複製コンテキストで実行された継続処理は、そのデータにアクセスすることができます:
import io.smallrye.common.vertx.ContextLocals;
AtomicInteger counter = new AtomicInteger();
public Uni<String> invoke() {
Context context = Vertx.currentContext();
ContextLocals.put("message", "hello");
ContextLocals.put("id", counter.incrementAndGet());
return invokeRemoteService()
.emitOn(runnable -> context.runOnContext(runnable))
.map(res -> {
// Can still access the context local data
// `get(...)` returns an Optional
String msg = ContextLocals.<String>get("message").orElseThrow();
Integer id = ContextLocals.<Integer>get("id").orElseThrow();
return "%s - %s - %d".formatted(res, msg, id);
});
}
先ほどのコード・スニペットでは io.smallrye.common.vertx.ContextLocals を使い、ローカル・データへのアクセスを簡素化しています。
Vertx.currentContext().getLocal("key") を使ってもアクセスできます。
|
コンテキストローカルデータは、リアクティブ実行中にオブジェクトを伝播する効率的な方法を提供します。 トレースメタデータ、メトリクス、セッションを安全に保存し、取り出すことができます。
コンテクスト・ローカルの制限
ただし、このような機能は複製コンテキストでのみ使用する必要があります。 上述したように、コードにとっては透過的です。 複製コンテキストはコンテキストなので、同じAPIを公開します。
Quarkusでは、ローカルデータへのアクセスは複製コンテキストに制限されています。
ルートコンテキストからローカルデータにアクセスしようとすると、 UnsupportedOperationException
がスローされます。
これにより、異なる処理ユニット間で共有されているデータへのアクセスを防ぎます。
java.lang.UnsupportedOperationException: Access to Context.putLocal(), Context.getLocal() and Context.removeLocal() are forbidden from a 'root' context as it can leak data between unrelated processing. Make sure the method runs on a 'duplicated' (local) Context.
セーフコンテキスト
コンテキストに セーフ マークをつけることができます。 これは、他のエクステンションがどのコンテキストが隔離されているかを識別し、固有のスレッドによるアクセスを保証するために統合するためのものです。 Hibernate Reactive はこの機能を使用して、現在開いているセッションを保存するために現在のコンテキストが安全かどうかをチェックし、意図せず同じセッションを共有する可能性のある複数のリアクティブ操作を誤ってインターリーブすることからユーザーを保護します。
Vert.x Web は、各 http Web リクエストに対して新しい複製コンテキストを作成します。Quarkus REST は、そのようなコンテキストに対し、安全であることをマークします。 他のエクステンションも、ローカルコンテキストに安全に使用できる新しいコンテキストを設定する際は、同様のパターンに従う必要があります。そうすることで、コンテキストは順番に使用され、同時にアクセスされません。また、現在のリアクティブチェーンにスコープ指定され、コンテキストオブジェクトを明示的に渡す必要がなくなり、利便性が向上します。
他の場合では、現在のコンテキストに対して安全ではないことを明示的にマークすると役立つかもしれません。たとえば、既存のコンテキストを複数のワーカー間で共有して、いくつかの操作を並列処理する必要がある場合、同じコンテキストを適切にマークまたはアンマークすることで、そのコンテキストに安全な期間と、それに続く安全ではない期間を設定できます。
コンテキストをセーフとしてマークするには、次のようにします:
-
io.quarkus.vertx.SafeVertxContext
アノテーションを使用します。 -
io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle
クラスを使用します。
io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle
クラスを使用すると、現在のコンテキストを明示的に safe
または unsafe
としてマークできます。さらに、新規コンテキストのデフォルトの状態である unmarked
もあります。
デフォルトでは、システムプロパティー io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.UNRESTRICTED_BY_DEFAULT
が true
に設定されていない限り、マークされていないコンテキストは unsafe
とみなされます。
SafeVertxContext
アノテーションは、現在の複製されたコンテキストを安全であるとしてマークし、コンテキストが unmarked
の場合、もしくはすでに safe
とマークされている場合に、アノテーションが追加されているメソッドを呼び出します。
コンテキストが unsafe
としてマークされている場合は、force=true
パラメーターを使用して強制的に safe
にできます。
ただし、これは慎重に実行する必要があります。
@SafeVertxContext アノテーションは、CDI インターセプターバインドアノテーションです。
したがって、CDI Bean および非プライベートメソッドに対してのみ機能します。
|
複製コンテキストをサポートするエクステンション
一般的に、Quarkusは複製コンテキストに対してリアクティブコードを呼び出します。 そのため、ローカルデータに安全にアクセスできます。 これは以下に適用されます:
-
Quarkus REST
-
gRPC
-
Reactive Routes
-
Vert.x Event Bus
@ConsumeEvent
-
RESTクライアント
-
リアクティブ・メッセージング(Kafka、AMQP)
-
Funqy
-
QuarkusスケジューラとQuartz
-
Redisクライアント(pub/subコマンド用)
-
GraphQL
ルート・コンテキストと複製コンテキストの区別
ルート・コンテキストと複製コンテキストは、以下の方法で区別できます:
boolean isDuplicated = VertxContext.isDuplicatedContext(context);
このコードでは、 io.smallrye.common.vertx.VertxContext
ヘルパークラスを使用しています。
複製コンテキストとマッピングされた診断コンテキスト(MDC)
ロガーを使用する場合、MDC(ログ メッセージに追加されるコンテキスト データ)は、利用可能な場合、複製コンテキストに保存されます。 詳細については、 ロギング リファレンス ガイド を確認してください。
CDI リクエストスコープ
Quarkusでは、CDIリクエストスコープは複製コンテキストに保存されます。つまり、リクエストのリアクティブ処理と同時に、CDIリクエストスコープが自動的に伝搬されます。
リアクティブメッセージング
Kafka と AMQP コネクタは、 メッセージコンテキスト を実装し、各メッセージに対して複製コンテキストを作成します。 このメッセージコンテキストは完全なメッセージ処理に使用されるため、データの伝播に使用できます。
詳しくは Message Context のドキュメントを参照してください。