Skip to content

背景処理の実行文脈をリクエストごとに持つ (#317) - #480

Merged
kojira merged 2 commits into
mainfrom
fix/defer-background-ctx-317
Sep 4, 2026
Merged

背景処理の実行文脈をリクエストごとに持つ (#317)#480
kojira merged 2 commits into
mainfrom
fix/defer-background-ctx-317

Conversation

@kojira

@kojira kojira commented Sep 4, 2026

Copy link
Copy Markdown
Member

何が壊れていたか

deferBackground() が参照する ExecutionContextruntime.ts のモジュール変数に置いていた。Cloudflare Workers は1つのアイソレートで複数リクエストを同時に捌くため、

  1. リクエストA がログイン処理に入り、bindEnv(env, ctxA)_ctx = ctxA
  2. A が外向き fetch(連携先のアイコン取得など)で待ちに入る
  3. その間にリクエストB が来て bindEnv(env, ctxB)_ctx = ctxB上書き
  4. A が再開して deferBackground(work) を呼ぶと、A の仕事が B の waitUntil に載る

B のレスポンスが先に終わると、A の背景処理は走り切る前に打ち切られうる。運用側から見える形は「送ったはずのメールが出ない」「取り込んだはずのアイコンが保存されない」「最終アクセス時刻が記録されない」で、いずれも例外は出ないので静かに落ちる#313 でアイコン取り込みがログインのホットパスに載ったため、踏む頻度が上がっていた。

直し方

実行文脈を AsyncLocalStorage に移し、Worker のエントリ(fetch / scheduled)で1回だけ張る。

  • apps/server/src/runtime.ts:57ctxStoredeferBackgroundctxStore.getStore() から自分のリクエストの ctx を取る
  • apps/server/src/runtime.ts:65runWithExecutionContext(ctx, fn)
  • apps/server/src/worker.ts:642fetch が張る。bindEnv から ctx 引数を落とした
  • apps/server/src/worker.ts:659scheduled(cron)も自分の文脈を張る。以前は文脈なしでその場 await に落ちていた

Hono の Context を引き回さなかった理由: deferBackground の呼び出し元は db/repositories/notifications.ts などリポジトリ層まで深い。c を引数で通すと db 層が HTTP フレームワークに依存し、通り道の全ハンドラを書き換えることになる。AsyncLocalStorage なら呼び出し側の書き方を一切変えずに文脈だけをリクエストへ閉じ込められる(nodejs_compat は既に有効)。到達経路を1本に保つため、モジュール変数の控えは残していない。文脈の外(テストからの直接呼び出し)では従来どおりその場で await する。

テストが噛むことの確認

apps/server/test/runtime-execution-context.test.ts を追加。A が待っている間に B が丸ごと走り切る状況を作り、A の背景処理が A の ctx に付くことを見る。

仕組みだけを修正前(モジュール変数)に戻すと落ちる:

 FAIL  test/runtime-execution-context.test.ts > 背景処理の実行文脈はリクエストごと (#317) > A が待っている間に B が入っても、A の背景処理は A の ctx に付く
AssertionError: expected [] to deeply equal [ Promise{…} ]

- Expected
+ Received

- [
-   Promise {},
- ]
+ []

A の ctx は空=A の仕事は B の ctx に載っていた、という #317 そのものの形。

監査した呼び出し元

deferBackground の全10か所(一斉連絡、アイコン取り込み、最終アクセス時刻、通知作成2か所、事前アンケート、スケジュール資料メタ2か所、一斉連絡の再送、アイコン配信キャッシュ)を確認。いずれも HTTP ルートか cron からしか到達せず、モジュールスコープで走るものは無い。cron は文脈を張るようにしたので、リマインダー配下の背景処理も自分の ctx に載る。

メール送信・OG取得の予算takeEmailSlot / takeOgFetchSlot)はアイソレート共有のモジュール変数のまま。暴走防止の安全弁で、多めに数えて送信を控える側に倒れるだけなので取りこぼしにはならない。設計doc に明記した。

設計doc

docs/design.md に「バックグラウンド処理の実行文脈 (#317)」節を追加。なぜリクエストごとに持つ必要があるか、なぜ AsyncLocalStorage か、文脈を張るのはエントリ1か所だけ、という制約を書いた。

Closes #317

🤖 Generated with Claude Code

https://claude.ai/code/session_01SLkuubF8cHzSnnsv4fN9DA

kojira and others added 2 commits September 4, 2026 22:50
deferBackground が参照する ExecutionContext をモジュール変数に置いていた。
Workers は1つのアイソレートで複数リクエストを同時に捌くため、リクエストAが
ネットワーク待ちをしている間にBが bindEnv すると、Aの背景処理がBの ctx に
waitUntil される。Bのレスポンスが先に終われば、Aの背景処理は走り切る前に
打ち切られる(送ったつもりのメールが出ない、取り込んだつもりのアイコンが
保存されない、最終アクセス時刻が記録されない)。#313 でログインのホット
パスに載ったため踏みやすくなっていた。

実行文脈を AsyncLocalStorage に移し、Worker のエントリ(fetch / scheduled)
で1回だけ張る。deferBackground の呼び出し元はリポジトリ層まで深く、Hono の
Context を引き回すと db 層が HTTP フレームワークに依存するため、呼び出し側の
書き方を変えずに文脈だけをリクエストへ閉じ込められる AsyncLocalStorage を
選んだ。到達経路を1本に保つため、モジュール変数の控えは残していない。

cron も自分の文脈を張る。以前は文脈なしでその場 await に落ちていた。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SLkuubF8cHzSnnsv4fN9DA
1. cron の文脈を張る行が誰にも押さえられていなかった。sendEventReminders 自身は
   deferBackground を呼ばないため、あの行を消しても全テストが通ってしまう。
   scheduled ハンドラを直接呼び、中の deferBackground が cron の ctx に載ることを見る。
2. fetch の文脈も、消したときに落ちるのが staff-timeline のOGメタ更新という
   無関係なテストだけだった。認証を通したときのアクセス記録がそのリクエストの
   ctx に載ることを直接見る。
3. design.md はアイソレート共有の予算が「送信を控える側にしか倒れない」と
   書いていたが、bindEnv は毎リクエスト予算を 20 に戻すので、並行リクエストが
   入ると走っている側の予算まで回復して 20 を超えて送りうる。両方向を書いた。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SLkuubF8cHzSnnsv4fN9DA
@kojira
kojira merged commit cd8df4f into main Sep 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

deferBackground が参照する ctx がモジュールグローバルで、別リクエストの ctx に waitUntil しうる

1 participant