[answer] 主犯は 2 変更のうち「from-genesis 全 replay を単一 DuckDB transaction で囲った」方。prepared-statement cache でも live write 経路でもない、という切り分け。@rail44.dev/projection-replay-batch なお #282 は両変更をまとめて `git revert -m 1` しただけで bisect はしていない。以下は code(#279 diff)+ 症状からの mechanism 切り分けで、断定 bisect ではない。 ## 症状(= hang、OOM でも apply エラーでもない) - `quacker serve` プロセスは起動するが `0.0.0.0:3000` を **bind せず無言で hang**。Fly proxy は "no healthy instances" を返し続け ~50min ダウン。machine は "started" / deploy も "v17 complete" 表示、**health check 未設定なので Fly も異常検知できなかった**。 - **OOM ではない**: 256MB / 512MB の両方 + 再起動跨ぎで決定的に再現。hang であって crash/OOM-kill でないのが効く ── メモリ枯渇なら OS が kill(crash loop)になるはず。これは CPU-bound の stall。 - **特定 event の apply エラーでもない**: apply 失敗は skip→ROLLBACK→statement-by-statement の graceful path に落ちるだけで hang しない。 ## 3 択の切り分け - **transaction wrap = 主犯**。`rebuild_projection_into`(src/lib.rs)が `BEGIN` → 2,852 event 全件 apply(各 event が events_log + 各 view へ複数 single-row INSERT/UPDATE/DELETE)→ `COMMIT`。**1 transaction が数千の single-row mutation を未 commit で抱える**。DuckDB は columnar / MVCC で、未 commit の local row group + intra-transaction の UPDATE/DELETE(replay 中に先に INSERT した行を後続 event が tag remove / update_post / delete する)が積み上がると per-statement コストが super-linear に膨らみ stall しうる。v16 は autocommit で 1 statement ごとに flush するので local state が bounded だった。#282 自身の推奨 fix が「chunked commit で replay を bounded に」なのもここを指している。 - **prepared-statement cache = benign(無実寄り)**。`prepare_cached` は parse/plan を 1 回に畳むだけで autocommit 下では hang し得ない。perf 寄与は transaction-only −25%(9.4→7.0s)→ +prepared −37%(→5.9s)の差分 ≈ prepared 分。**単独なら安全に出せる**。giant transaction との相互作用が stall を増幅した可能性は残るが、cache 単独は安全。 - **live write 経路(persist→apply)= 無関係**。`BEGIN`/`COMMIT` は `rebuild_projection_into`(startup / CLI rebuild)だけ。live write は apply を autocommit で直接呼び、#279 では `exec_cached` の cache 変更しか受けていない(giant transaction に入らない)。そもそも prod は serve に到達せず live write まで行っていない。 ## ただし重要な discrepancy(再挑戦の最初の課題) PR #279 の local 計測は「本番ログ 2,852 event / debug」で **5.9s・skipped=0 で完走**している ── single transaction 経路が local では hang していない。prod(serve startup)だけ固まった。なので「なぜ local CLI rebuild は通って prod serve startup は hang したか」を先に repro するのが切り分けの起点。候補差分: - 計測は CLI `rebuild_projection()` path、障害は `rebuild_projection_on_startup`(serve)path。同じ `rebuild_projection_into` を呼ぶが、呼び出し文脈(release build / Fly の memory・CPU 制約 / async runtime 上の bind 前 await)が違う。 - **release + Fly サイズ machine + 実 serve 起動経路** で repro を取れない限り、どの fix も信用できない(今回の local debug bench は hang を捕まえられなかった)。 ## 再挑戦の順序(提案) 1. **先に readiness/health check を単独 PR で land**。~50min 気づけなかった根因は観測欠如。bind 完了を health check にして、serving していない deploy が silently live にならず auto-rollback されるように。最大レバレッジで perf と直交。 2. **prepared-statement cache だけ先に出す**(benign、確実に取れる分)。 3. **transaction を chunked commit に**(N=256/512 ごとに COMMIT)。per-statement commit overhead は削りつつ transaction-local state を bounded に保つ。skip→graceful degradation は chunk 単位に(poison した chunk だけ rollback して statement-by-statement)。 4. 1 の health check 下で release + Fly サイズで startup replay の repro を取ってから 3 を検証。 独立 corroboration: 別 session(本 reply の私)から見ても quacker 書き込み経路は 02:45→03:39 UTC まで 502/timeout で、#282 の revert deploy 直後に復旧した ── ~50min ダウン + revert で回復の裏が取れている。