AsyncLocalStorage とは(おさらい)
この章では、AsyncLocalStorage の API を 30 秒で振り返る。すでに run() / getStore() を書いたことがある読者向けに、知っているはずの内容を素早く確認する。重要なのは API の中身そのものではなく、**この章で生まれる「素朴な疑問」**だ。それが第 2 章以降の旅の起点になる。
ALS は何を解決するか
Node.js は伝統的に「リクエスト」という単位を持たない。http.createServer のコールバックが来るたびに、各リクエストは独立した実行コンテキストで処理されるが、それを横断的に追跡する仕組みは long-running なプロセスには存在しなかった。
スレッドベースの言語(Java, Ruby)には Thread-Local Storage がある。だが Node.js は単一スレッドで多数のリクエストを async で捌く。スレッドという入れ物がない以上、「このリクエスト固有のデータ」を関数引数で渡し続けない限り、ロガーや認可ミドルウェアからリクエスト ID を引くことができなかった。
これを解決するのが AsyncLocalStorage(以下 ALS)。スレッドの代わりに「非同期の実行コンテキスト」を入れ物にする。
API は 4 つだけ
ALS の API は本質的に 4 つしかない。
import { AsyncLocalStorage } from 'node:async_hooks';
const als = new AsyncLocalStorage<{ requestId: string }>();
// 1. run: スコープ付きで context を伝播
als.run({ requestId: 'r-001' }, () => {
// 2. getStore: 現在の context を取得
console.log(als.getStore()?.requestId); // 'r-001'
setTimeout(() => {
console.log(als.getStore()?.requestId); // 'r-001' ← 非同期を超える
}, 100);
});
console.log(als.getStore()); // undefined: スコープ外
これだけで「await を超えてコンテキストが見える」が成立する。最初に見ると魔法のようだが、なぜ動くのかは第 4-5 章で実装レベルで解く。
run(store, callback, ...args)
スコープ付き。callback の実行中(およびそこから派生した非同期処理)でのみ store が見える。callback を抜けると消える。例外もスコープを抜ける:
try {
als.run({ id: 1 }, () => {
throw new Error('boom');
});
} catch (e) {
als.getStore(); // undefined: スコープ外
}
enterWith(store)
run と違ってスコープなし。呼んだ後ずっとそのコンテキストになる。明示的に exit() するか別の run() で囲まない限り、現在の async chain 全体に影響する。
als.enterWith({ id: 1 });
als.getStore(); // { id: 1 }
// この後で発生する全ての async も { id: 1 } を見る
これは便利に見えて罠になる。Express のようなミドルウェア構造で enterWith を使うと、リクエスト間でコンテキストが漏れる事故が起きる。基本は run() を使い、enterWith は本当に必要な時だけ使う。
exit(callback, ...args)
run の中から一時的にコンテキスト外で何かを実行したいときに使う。callback の中では getStore() が undefined を返す。
als.run({ id: 1 }, () => {
als.getStore(); // { id: 1 }
als.exit(() => {
als.getStore(); // undefined
// ここはコンテキスト外
});
als.getStore(); // { id: 1 }: 復帰
});
getStore()
現在の store を返す。スコープ外なら undefined。O(1) で locking なしの参照(V8 の async context slot を直接読む)。
ライフサイクル図
run() の挙動を sequence で見ると、こうなる。
sequenceDiagram
participant U as user code
participant A as ALS
participant V8 as V8 / async context
U->>A: als.run(store, callback)
A->>V8: enter context (push store)
A->>U: invoke callback()
Note right of U: callback 内 await・<br/>setTimeout 内でも<br/>als.getStore() は store を返す
U->>A: callback returns
A->>V8: exit context (pop store)
Note right of A: スコープ外: getStore() は<br/>undefined を返す
ここで湧く素朴な疑問:callback の中で await した先や setTimeout の callback、つまり呼び出しスタックが完全に切れている場所で、なぜ V8 は「正しい store」を返せるのか。
この章の要点
- ALS は Node.js の単一スレッド前提で「リクエストスコープのコンテキスト」を実現する仕組み
- API は
run/enterWith/exit/getStoreの 4 つだけ enterWithはスコープなしで便利だが、リクエスト間漏洩を起こす罠あり。基本はrungetStore()は O(1)、locking なし
次章への問いかけ
awaitをまたいでもgetStore()が正しい値を返す。なぜ?その答えは、ALS が乗っかっている下のレイヤー、
async_hooksの仕組みにある。次章で見ていく。