背景処理の実行文脈をリクエストごとに持つ (#317) - #480
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
何が壊れていたか
deferBackground()が参照するExecutionContextをruntime.tsのモジュール変数に置いていた。Cloudflare Workers は1つのアイソレートで複数リクエストを同時に捌くため、bindEnv(env, ctxA)で_ctx = ctxAbindEnv(env, ctxB)→_ctx = ctxBに上書きdeferBackground(work)を呼ぶと、A の仕事が B のwaitUntilに載るB のレスポンスが先に終わると、A の背景処理は走り切る前に打ち切られうる。運用側から見える形は「送ったはずのメールが出ない」「取り込んだはずのアイコンが保存されない」「最終アクセス時刻が記録されない」で、いずれも例外は出ないので静かに落ちる。#313 でアイコン取り込みがログインのホットパスに載ったため、踏む頻度が上がっていた。
直し方
実行文脈を
AsyncLocalStorageに移し、Worker のエントリ(fetch/scheduled)で1回だけ張る。apps/server/src/runtime.ts:57—ctxStore。deferBackgroundはctxStore.getStore()から自分のリクエストの ctx を取るapps/server/src/runtime.ts:65—runWithExecutionContext(ctx, fn)apps/server/src/worker.ts:642—fetchが張る。bindEnvから ctx 引数を落としたapps/server/src/worker.ts:659—scheduled(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 に付くことを見る。仕組みだけを修正前(モジュール変数)に戻すと落ちる:
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