プロンプトとコンテキスト
ある Node.js サービスは requestId、テナント情報、監査データを AsyncLocalStorage に保存しています。HTTP エントリではストアを読み取ることができますが、データベースドライバのコールバック、イベントエミッター、カスタム Promise ライクオブジェクト、またはワーカー境界の後で一部のログがコンテキストを失います。チームは Node.js 22 から 24 にアップグレードしたばかりで、デフォルト実装の変更が関係しているかどうかを知りたがっています。診断、修正、リグレッションテスト、およびフォールバック計画を設計してください。
Node.js 24 のリリースノートには、AsyncLocalStorage がデフォルトで AsyncContextFrame を使用するようになったと記載されています。公式ドキュメントでは、非同期処理を適切な実行コンテキストに関連付けるために、コールバックベースの API やカスタム thenable には依然として AsyncResource が必要な場合があると説明されています。この面接では、すべての消失の原因を実行環境のせいにするのではなく、非同期境界、オブザーバビリティ、およびバージョン移行への理解を評価します。
面接官が見ているポイント
面接官は、run() によって作成されたコンテキスト、非同期リソースのライフタイム、スレッド間境界、およびストアを上書きするビジネスコードの違いを明確に理解しているかを評価します。優れた回答では、最初の破損箇所を見つけるために最小限の再現環境を使用し、無分別な enterWith() の使用を避け、サードパーティのコールバック、エラーパス、サンプリングされた診断、および Node の各バージョンに対する検証を定義します。
確認すべき明確化のための質問
- コンテキストの消失は、同一のイベントループ内、ワーカー、子プロセス、またはネットワーク境界のどこで発生していますか?
- ライブラリはネイティブ Promise、コールバック、イベントエミッター、カスタム thenable のどれを使用していますか?
- 別の
run()、enterWith()、または再利用された非同期タスクによってストアが誤って上書きされていませんか? - requestId の消失は、監査や請求の正確性に影響しますか、それともログの関連付けのみに影響しますか?
- 起動フラグ、依存関係のバージョン、実験的スイッチは Node.js 22 と 24 で同一ですか?
30秒の回答
「Node 24 のせいにするのではなく、エントリ、すべての非同期境界、最終ログで不変のストアスナップショットを記録し、最小限の再現コードを用いて最初の消失箇所を特定します。ネイティブ Promise チェーンでは、リクエスト境界で run() を使用すべきです。サードパーティのコールバックやカスタム thenable でコンテキストが伝播しない場合は、タスクの作成場所とそのコールバックの実行場所で AsyncResource を使用します。グローバルな enterWith() による汚染を避け、Node 22/24、エラー、タイムアウト、ワーカーを個別にリグレッション検証します。修正の有効性は、requestId の完全性とビジネスロジックの正確性によって証明されます」
ステップバイステップの詳細な回答
1. コンテキストの規約(コントラクト)を定義する
ストアのフィールド、ライフタイム、不変性を指定します。リクエストごとに新しいストアを作成します。後続のコードは値を読み取ったり派生させたりすることはできますが、リクエスト間で可変オブジェクトを共有してはなりません。requestId の欠落が単なるログの問題なのか、それとも認可やテナントの分離に影響するのかによって、停止条件と修正の優先度が決まります。
2. スナップショットを用いて最初の消失箇所を特定する
エントリ、データベース呼び出しの前後、イベントリスナー内、Promise コールバック、タイムアウト、エラーハンドラー、最終ログにおいて、getStore() からフィールドのサマリーを取得します。機密性の高いテナントデータは決して含めず、ハッシュまたは短いリクエスト ID のみをログに記録します。各非同期境界にラベルを付け、最後のエラーだけを調べるのではなく、値から undefined に切り替わる最初のポイントを特定します。
import { AsyncLocalStorage } from 'node:async_hooks';
const requestContext = new AsyncLocalStorage();
function contextSnapshot(label) {
const store = requestContext.getStore();
return { label, requestId: store?.requestId ?? null };
}3. run()、enterWith()、およびリソース関連付けを区別する
run(store, callback) はコールバックおよびそれが作成する非同期処理内でストアを提供するため、適切なリクエスト境界となります。enterWith() は同じ同期的実行内の後続のイベント処理にコンテキストを拡張し、リスナーを汚染する可能性があるため、境界が明確でないまま汎用的な修正として使用すべきではありません。非同期リソースを適切に作成しないコールバックライブラリには、AsyncResource によるラッパーが必要です。
4. サードパーティのコールバック境界をラップする
まず、ライブラリがすでに Node の非同期リソースを適切に使用しているかどうかを確認します。適切に使用されていない場合は、タスクごとに 1 つの AsyncResource を作成し、runInAsyncScope でコールバックを呼び出し、タスク完了時にリソースを破棄します。複数のリクエストで 1 つのリソースを再利用したり、ロガーにだけ requestId をパッチしたりしてはなりません。それでは実際のコンテキスト破損が隠蔽されてしまいます。
import { AsyncResource } from 'node:async_hooks';
function bindCallback(callback) {
const resource = new AsyncResource('third-party-callback');
return (...args) => resource.runInAsyncScope(callback, null, ...args);
}5. thenable、イベント、ワーカーを個別にテストする
カスタム thenable はネイティブ Promise のコンテキスト伝播に従わない場合があります。イベントエミッターは後続の tick でリスナーを呼び出すことがあります。ワーカーや子プロセスは独立した実行コンテキストを持ち、ストアを暗黙的に共有することはできません。各境界に対して最小限のテストを作成し、明示的なメッセージを介して受け渡されるフィールドと、新しいリクエスト境界で再作成されるフィールドを明確に文書化します。
6. Node 22/24 のリグレッション検証と結果の観察
アップグレードマトリクスに Node 24 のデフォルト実装の変更を含めますが、根本原因分析を単なるバージョン比較で済ませてはなりません。起動フラグ、依存関係のバージョン、実行モードを統一した上で、requestId の完全性、境界破損、エラー率、レイテンシ、スループットを比較します。リリース後も低レートの境界診断を継続し、認可やテナントのフィールドが消失した場合はカナリアリリースを停止して安定バージョンにロールバックします。
高品質な模範解答
ストアの規約を定義し、値から空への最初の変化を特定するために、エントリ、主要な非同期境界、最終ログで非機密なスナップショットを記録します。ネイティブ Promise チェーンでは、リクエスト境界で run() を使用します。サードパーティのコールバックやカスタム thenable がコンテキストを伝播しない場合、ラッパーはタスクごとに 1 つの AsyncResource を作成し、runInAsyncScope でコールバックを呼び出してから破棄します。グローバルな enterWith() で問題を覆い隠すことはしません。イベントエミッター、タイムアウト、エラー、ワーカー、および Node 22/24 を個別にテストし、requestId の完全性とビジネスフィールドを比較します。Node 24 の AsyncContextFrame はテストマトリクス内のバージョン変数であり、それだけが唯一の原因ではありません。
よくある間違い
- すべての消失を Node 24 のせいにする → サードパーティの境界が破損原因である可能性があります → まずは最小限の再現と境界スナップショットを使用する。
- いたるところで
enterWith()を呼び出す → イベントリスナー同士が相互汚染する可能性があります → リクエストスコープのrun()を優先する。 - ロガーにのみ requestId を追加する → テナントや監査のコンテキストは失われたままです → 非同期リソースの関連付けを修復する。
- すべてのリクエストで 1 つの
AsyncResourceを再利用する → コンテキストが相互汚染します → タスクごとに作成して破棄する。 - ワーカーがストアを継承すると想定する → スレッド間のコンテキストは暗黙的ではありません → 必要なフィールドをメッセージで明示的に渡す。
フォローアップ質問と回答
run() と enterWith() はどのように使い分けますか?
リクエストやタスクの境界には run() を優先します。スコープがコールバックおよびその非同期処理に従うためです。enterWith() は現在の同期的実行における後続のイベント処理に影響を与えるため、境界が明確であり、リスナーの汚染が排除されている場合にのみ使用すべきです。
AsyncResource はどのような場合に必要ですか?
コールバック API、イベントラッパー、またはカスタム thenable が、その非同期処理を Node の非同期リソースグラフに接続できていない場合に使用し、タスクが作成されたコンテキスト内でコールバックが実行できるようにします。
ワーカー内で requestId を維持するにはどうすればよいですか?
ワーカーは独立した実行コンテキストを持つため、メッセージで requestId または最小限のテナント識別子を渡し、ワーカーのエントリで新しい AsyncLocalStorage ストアを作成します。機密オブジェクトの送信は避けます。
Node 24 の AsyncContextFrame はすべてのコンテキスト消失を解決しますか?
いいえ。デフォルトの実装は変わりますが、サードパーティのコールバック、誤った enterWith()、カスタム thenable、スレッド間境界には依然として個別のテストが必要です。
修正によってオーバーヘッドが増加していないことをどのように証明しますか?
同一のトラフィックと依存関係バージョンの下で、レイテンシ、スループット、CPU、メモリ、requestId の完全性を比較し、単一のベンチマークに依存するのではなく、バインドされたコールバック用に作成されたリソースの数を観察します。