The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

複製コンテキスト、コンテキストローカル、非同期処理と伝播

伝統的な、ブロッキング、同期フレームワークを使う場合、各リクエストの処理は専用のスレッドで実行されます。 つまり、処理全体に対して同じスレッドが使われます。 このスレッドは、処理が完了するまで他の実行に使われることはありません。 セキュリティプリンシパルやトレース ID のような、処理に沿ったデータの伝搬が必要な場合は、 ThreadLocals を使うことができます。 伝搬されたデータは、処理が完了するとクリアされます。

リアクティブ実行モデルと非同期実行モデルを使用する場合、同じメカニズムを使用することはできません。 多くのプロセススレッドを使用せず、リソースの使用量を減らす(アプリケーションの並行性を高める)ために、同じスレッドを使用して複数の並行処理を処理することができます。 したがって、 ThreadLocals を使用することはできません。さまざまな並行処理の間で値が漏れてしまうからです。

Vert.xの 複製コンテキスト は、同じような伝搬を提供する構造体ですが、非同期処理用です。同期コードでも使用できます。

このドキュメントでは、複製コンテキストについて、その取得方法、使用方法、(非同期)処理での伝搬方法を説明します。

リアクティブ・モデル

このセクションはリアクティブモデルの説明ではありません。詳細については、 Quarkusリアクティブアーキテクチャ を参照してください。

Quarkusはリアクティブエンジンを採用しています。 このエンジンは、最新のコンテナ化されたクラウドネイティブなアプリケーションに対応する効率性と並行性を提供します。

たとえば、Quarkus REST(旧RESTEasy Reactive)やgRPCを使用すると、QuarkusはI/Oスレッドでビジネスロジックを呼び出すことができます。 これらのスレッドは イベントループ と名付けられ、 マルチリアクターパターン を実装します。

命令型モデルを使用する場合、Quarkusは各処理ユニット(HTTPリクエストやgRPC呼び出しなど)にワーカースレッドを関連付けます。 このスレッドは、処理が完了するまで、この特定の処理に専念します。 したがって、 スレッドローカル を使用して、処理に沿ってデータを伝搬することができます。 現在の処理が完了するまで、他の処理ユニットがそのスレッドを使用することはありません。

リアクティブ・モデルでは、コードはイベントループのスレッドで実行されます。 これらのイベントループは、複数の同時処理ユニットを実行します。 例えば、同じイベントループで複数のHTTPリクエストを同時に処理することができます。 次の図は、このリアクティブ実行モデルを示しています:

Continuation in the reactive execution model

これらのイベントループは 絶対に ブロックしてはいけません。 もしブロックしてしまうと、モデル全体が崩壊してしまいます。 このように、HTTPリクエストの処理がI/O操作(外部サービスの呼び出しなど)を実行する必要がある場合、その処理は:

  1. 操作をスケジュールします、

  2. 継続(I/O が完了したときに呼び出すコード)を渡します、

  3. スレッドを解放します。

そのスレッドは別の同時リクエストを処理できます。 スケジュールされた操作が完了すると、渡された継続を 同じイベントループで 実行します。

このモデルは特に効率的で(スレッド数が少なく)、パフォーマンスも高い(コンテキストスイッチの回避)です。 しかし、異なる開発モデルが必要で、 スレッドローカルを 使うことはできません。 実際、これらはすべて同じスレッド、つまりイベントループで処理されます。

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 Produces: Hello 1, you are running on context: io.vertx.core.impl.DuplicatedContext@5dc42d4f and on thread Thread[vert.x-eventloop-thread-1,5,main] - so invoked on an event loop.
2 Produces: Hello 2, you are running on context: io.vertx.core.impl.DuplicatedContext@41781ccb and on thread Thread[executor-thread-1,5,main] - so invoked on a worker thread.

With this Context object, you can schedule operations in the same context. The context is handy for executing the continuation on the same event loop (as contexts are attached to event loops), as described in the picture above. For example, when you need to call something asynchronous, you capture the current context, and when the response arrives, it invokes the continuation in that context:

public Uni<String> invoke() {
   Context context = Vertx.currentContext();
   return invokeRemoteService()
       .emitOn(runnable -> context.runOnContext(runnable)); (1)
}
1 Emit the result in the same context.
ほとんどのQuarkusクライアントは自動的にこれを行い、適切なコンテキストで継続を呼び出します。

コンテクストには2つのレベルがあります:

  • イベントループを表すルートコンテキストは、同時処理間でデータを漏らさずに伝播するために使用することはできません。

  • 複製コンテキストは、ルートコンテキストに基づきますが、共有されず、処理ユニットを表します。

このように、複製コンテキストは各処理ユニットに関連付けられています。 複製コンテキストは、依然としてルートコンテキストに関連付けられており、複製コンテキストを使用するスケジューリング操作は、関連付けられたルートコンテキストで実行されます。 しかし、ルートコンテキストとは異なり、処理ユニット間で共有されることはありません。 しかし、ある処理ユニットの継続は、同じ複製コンテキストを使用します。そのため、先ほどのコードスニペットでは、同じイベントループだけでなく、同じ複製コンテキストで呼び出されます(キャプチャされたコンテキストが複製コンテキストであると仮定します。)

Continuation with duplicated contexts

コンテキスト・ローカルデータ

複製コンテキストで実行された場合、コードは他の並行処理と共有することなくデータを保存できます。 そのため、ローカルデータの保存、取得、削除を行うことができます。同じ複製コンテキストで実行された継続処理は、そのデータにアクセスすることができます:

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 will create a new duplicated context for each http web request; Quarkus REST will mark such contexts as safe. Other extensions should follow a similar pattern when they are setting up a new context that is safe to be used for a local context guaranteeing sequential use, non-concurrent access, and scoped to the current reactive chain as a convenience not to have to pass a "context" object along explicitly.

In other cases, it might be helpful to mark the current context as not safe instead explicitly; for example, if an existing context needs to be shared across multiple workers to process some operations in parallel: by marking and un-marking appropriately the same context can have spans in which it’s safe, followed by spans in which it’s not safe.

コンテキストをセーフとしてマークするには、次のようにします:

  1. io.quarkus.vertx.SafeVertxContext アノテーションを使用します。

  2. Use the io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle class

By using the io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle class, the current context can be explicitly marked as safe, or it can be explicitly marked as unsafe; there’s a third state which is the default of any new context: unmarked. The default is to consider any unmarked context to be unsafe, unless the system property io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle.UNRESTRICTED_BY_DEFAULT is set to true;

The SafeVertxContext annotation marks the current duplicated context as safe and invokes the annotated method if the context is unmarked or already marked as safe. If the context is marked as unsafe, you can force it to be safe using the force=true parameter. However, this possibility must be used carefully.

The @SafeVertxContext annotation is a CDI interceptor binding annotation. Therefore, it only works for CDI beans and on non-private methods.

複製コンテキストをサポートするエクステンション

一般的に、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 のドキュメントを参照してください。

OpenTelemetry

OpenTelemetry エクステンションは、複製コンテキストにトレースを保存するので、リアクティブで非同期なコードを使用しているときでも、確実に伝搬します。

関連コンテンツ