fix(poller): add a run-wide subrequest budget to the wiki surface [poller, docs, tests] - #249
Conversation
…ller, docs, tests] wiki surface の予算はすべて repo 単位で、run 全体の総和を見る主体がいなかった。 wiki cron は 1 invocation で POLL_REPOS を全て歩くため、各 repo が個別には規約を 守る一方で総和が Cloudflare の invocation 上限を超え、枯渇を最後尾の repo だけが 一身に受けていた。2026-08-15T03:45Z の tick では 6 番目の repo の raw content probe が全て例外になり、誰も見ていない page について 8 件の failure を記録していた。 変更点: - WIKI_SUBREQUEST_BUDGET_PER_RUN を新設し、runWikiSurfaces が各 pass の実測消費を 差し引きながら repo を歩く。除算ではなく減算なので、変更の無かった repo は 取り分をほぼ丸ごと後続へ渡す。既存の repo 単位予算は維持し、その上に被せる形。 - 残予算が pass を賄えない repo は呼び出し自体を行わない。cursor は据え置かれ、 次の run が同じ page から再開する。枯渇を failed として記録しない。 - WikiPollSummary に subrequests と inconclusive を追加。probe が例外を投げた page は「観測していない miss」として inconclusive に計上し、404 を観測した failed と分離した。"all candidates 404" という誤誘導的な文言を置き換えた。 - rotateReposForRun で処理順を tick ごとに回転させる。固定順のままだと常に 最後尾が痩せ、結果を決めるのが loop 上の位置だという元の欠陥が残るため。 offset は scheduledTime から導出するので watermark も schema 変更も不要。 取り分の値について: 定数は call site から読み取った会計単位であり、Cloudflare の 課金単位ではない。この会計では、失敗した run は 6 番目の repo が例外を投げた時点で 消費約 300 であり、文書化された上限 1000 には届いていない。差は poller 側からは 説明できないため、少なくとも 1 つの binding が 1 より高く課金されている。よって 取り分は唯一の硬い観測(300 未満で枯渇)に対して較正し 250 とした。1000 に合わせると この単位では到達不能な値になり、上限が永久に発火しない。 要件記述(docs/0-requirements.md / .ja.md)を同一 PR で更新し、poller.ts と docs に 残っていた "invocation budget of 1000" を前提とした安全性の主張を、宣言した取り分を 参照する形に直した。 #248
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
github-rag-mcp | 90355f8 | Aug 15 2026, 05:59 AM |
…wiki response sample backfill-wiki の response sample は WikiPollSummary をそのまま返すため、 新設した subrequests / inconclusive の 2 フィールドが欠けたままだった。 sample と運用メモの両方を実際の返却形に合わせ、観測した miss (failed) と 観測していない miss (inconclusive) の区別、および subrequests が cron 経路 でのみ run 全体の取り分から差し引かれる点を追記した。 #248
先送り警告の文言が「次の tick でこの run の先頭勢より前に出る」と述べていたが、 rotation の offset は tick あたり 1 なので、実際には 1 位ずつ繰り上がり、N tick 以内に先頭に立つ。ログが挙動を過大に述べていたのを実際の形に合わせた。 #248
…ler, docs, tests] 自己レビュー指摘への対応。指摘は ACCEPT。 ## 指摘の検証(自分で source を確認した) 指摘: `subrequests += WIKI_SUBREQUESTS_PER_EMBED` が未変更ページの `continue` より後にあるため、未変更ページは fetch 試行分しか課金されない。よって観測された run の消費は約 98 であり、取り分 250 は発火せず、run は失敗したときと同じ順序で 進む。テストが取り逃がしたのは `deepWiki` が全ページを変更済みとして提示し、 本番と費用プロファイルが逆になっているため。 source で確認した結果: - 加算位置は指摘のとおり。hash 一致 → `skipped++; continue;` の後に `subrequests += WIKI_SUBREQUESTS_PER_EMBED` がある。未変更ページの費用は fetch 試行分のみ。**指摘の核心は正しい。** - ただし「全 repo が 0 embedded だった」は issue 本文からは attested でない。 本文の表は log 行を抜粋しており `embedded` 欄を落としている。 したがって前提そのものは確認できなかった。 - しかし結論は前提なしで成立する。両極を計算すると、6 番目の repo が例外を 投げた時点の消費は 全ページ未変更で約 70、全ページ再 embed で約 292。 取り分 250 はこの幅の**内側**にあり、どちらのプロファイルかで発火するか しないかが変わる。すなわち依拠できない。しかも fetches == visited (repo 1/2/5)は prior が存在すること、つまり既索引=定常運転側の プロファイルを示唆する。**指摘の結論を受け入れる。** ## 採った remedy(提示された 3 案のうち 3 番目 + 追加) 1 案目(実消費の計測)は不可能。Workers は実行時に subrequest 消費量を 数える手段を提供していない。2 案目(観測点未満まで取り分を下げる)は却下。 下に 10 倍の未説明の差がある状態で 1 データ点に定数を当てはめる行為であり、 かつ何も起きていない idle run でも repo を先送りしてしまう。 3 案目を採り、その上で**枯渇の実行時検知**を追加した。これが本命である。 上限到達は見積もりではなく**観測できる** — binding が例外を投げ、名前を名乗る。 枯渇は invocation スコープかつ終端的なので(観測された run が probe ごとに 例外を投げ続けたことがその証拠)、最初の検知で run を終了するのが正しい応答。 - `isSubrequestExhaustion` を追加。`fetchWikiContent` / `probeWikiPageAlive` が 枯渇を通常の error と区別して報告する - 枯渇検知時: probe 中だったページは cursor 未前進のまま walk を break、 reap は丸ごと省略、`WikiPollSummary.exhausted` で報告 - `runWikiSurfaces` は `exhausted` を受けたら残り repo を全て未接触で先送りし run を終了する - ネットワークの一時障害は従来どおり per-page 軸(`inconclusive`)のまま。 1 回の blip で run 全体を止めない `WIKI_SUBREQUEST_BUDGET_PER_RUN` は残すが、**観測された事象への対処ではない** ことを定数の doc と要件記述に明記した。縛るのは bulk import の形であり、 値は観測幅の中での判断であって較正ではない。 ## 要求されたテスト `makeIndexedStore` を追加。全ページを hash 一致で store に事前投入するので、 walk が全ページを skip する定常運転プロファイルを再現する。追加 6 件: - 未変更ページが fetch 分しか課金されないこと(この非対称性が全ての起点) - **観測された 6 repo 構成の定常運転 run で取り分が発火しないこと**(否定的 結果の固定。将来これが落ちたら、単位の差が解決したか費用モデルが壊れたか のどちらかで、どちらも止まる価値がある) - 枯渇検知で run 全体が停止し、未到達 repo の cursor が未変更であること - 壁に当たったページで cursor が前進しないこと(reap 省略も確認) - 通常のネットワークエラーは per-page 軸に留まり run を止めないこと - `isSubrequestExhaustion` の分類(`429 Too Many Requests` を誤検知しない) 既存テストの `stubMultiWiki` は throw する文言を選べるようにした。従来は 枯渇文言を使っていたため、新しい分岐に当たって inconclusive の検証にならなく なっていた。 ## その他 - 指摘の「`remaining` が負になりうる」点: 確認済み、`wikiFetchBudgetForPass` が 0 を返すので健全。指摘どおり対応不要 - issue #248 の acceptance を改訂。criterion 1「`Too many subrequests` が 出ない」は最初の 1 回(= 検知イベント)を防げない以上、字義どおりには 達成不可能。達成すべき内容へ書き換え、単位が較正不能である事実を本文に 記録した - `backfill-wiki` の response sample に `exhausted` を追加 #248
smileygames
left a comment
There was a problem hiding this comment.
AI self-review (execution mode: auto) — 2 回目、pass
1 回目の self-review で 1 件指摘し、差し戻した。実装者は ACCEPT で adjudicate し、90355f8 で改訂済み。
指摘の帰結
指摘: 宣言された share (250) は定常運転のプロファイルでは発火せず、issue #248 の受け入れ条件 1 が満たされない。
改訂の方向は「予算を下げる」ではなく 「予算では直せないと結論し、枯渇の実行時検知を足す」 だった。この判断を支持する。予算の較正が不可能である理由(Workers に実行時カウンタが無い)は事実として確認でき、観測された 1 点に定数を合わせるのは同じ誤りの反復になる。
なお実装者は「全リポジトリが 0 embedded だった」という私の前提に、issue 本文からは裏付けられないという留保をつけ、保守側の幅(約 70〜292)で再計算して同じ結論に至っている。留保は正しい — 表から embedded 欄を落としたのは私の側の欠落である。事実としては tail ログに全リポジトリ 0 embedded が記録されており、実際の消費は下端(約 70〜98)に寄る。結論は変わらず、実装者の取った幅の方が保守的である。
検証したもの
| 項目 | 結果 |
|---|---|
| 枯渇時に cursor が動かない | pass — break は nextCursor = page.slug の前に置かれている |
| 枯渇時に reap が省略される | pass — listIndexedWikiPages の D1 呼び出し自体を回避 |
| 枯渇後に後続リポジトリが未接触 | pass — hitTheWall で continue、pollWiki を呼ばない |
hitTheWall が false に戻らない |
pass — true 以降は先頭で continue するため代入に到達しない |
| 通常の network error と識別される | pass — isSubrequestExhaustion で分岐、per-page 軸に留まる |
| reap probe 側の枯渇検知 | pass — continue 後、次反復先頭の break で追加 probe なし |
runWikiSurface の catch 経路 |
pass — summary を経ずに throw した枯渇も run を終了させる |
| 受け入れ条件の改訂 | pass — issue #248 本文で原文を取り消し線で残し、達成可能な形へ改訂済み |
特に評価する点
否定的結果を回帰テストで固定したこと。 does not engage the declared share on the observed steady-state run は「この予算はこの形には効かない」ことを pin するテストである。将来これが落ちたら、単位のズレが解消したか費用モデルが壊れたかのどちらかであり、どちらも停止に値する。守っているように読める定数を黙って残さない、という当初の要求に対する最も誠実な応答である。
stubMultiWiki の throw メッセージが枯渇文言のままだと新しい分岐に吸われて inconclusive の検証が空回りする、という自己申告も適切だった。
scope 逸脱
なし。rotation は issue が実装側の判断に委ねた項目であり、判断理由は記録されている。
次のステップ
auto モードにつき人間レビュー不要。self-review pass により直接 merge する。merge 後は Workers Builds の自動 deploy を経て、次の 45 * * * * tick が実地の観測点となる。観測すべきは Too many subrequests の消失ではなく、1 回の拒否が偽の page failure の連鎖に変わらないことである。
Closes #248
何が壊れていたか
wiki surface の予算はすべて repo 単位で、run 全体の総和を見る主体がいなかった。wiki cron は 1 invocation で
POLL_REPOSを全て歩くため、各 repo が個別には規約を守る一方で総和が Cloudflare の invocation 上限を超え、枯渇を最後尾の repo だけが一身に受けていた。枯渇は loop の性質であって、その repo の性質ではない。変更点
run 全体の予算.
WIKI_SUBREQUEST_BUDGET_PER_RUNを新設し、runWikiSurfacesが各 pass の実測消費を差し引きながら repo を歩く。除算ではなく減算 — diff surface のdiffFileBudgetPerPhaseが除算なのは phase の費用が事前に確定するからで、wiki の走査費用は走らせてみるまで分からない。おかげで変更の無かった repo は取り分をほぼ丸ごと後続へ渡す。既存の repo 単位予算は維持し、その上に被せる形(置き換えではない)。枯渇時は failed を記録しない. 残予算が pass を賄えない repo は呼び出し自体を行わない。cursor は据え置かれ、次の run が同じ page から再開する。
liplus-languageが既に示していた「上限に達したら cursor を残して正常終了」を run 全体の軸へ持ち上げた形。観測した miss と観測していない miss の分離.
WikiPollSummaryにsubrequestsとinconclusiveを追加。probe が例外を投げた page は絶対に「404 を観測した」ことにならないのでinconclusiveに計上し、failedから分離した。No content fetched ... (all candidates 404)の誤誘導的な文言を、例外と本物の 404 が区別できる 2 つの文言に置き換えた。inconclusiveでも cursor は前進させる(留めると恒久的に到達できない page 1 つが walk 全体を停止させうるため)。処理順の公平性(issue が判断を求めていた点). 入れることにした。固定順のままだと痩せるのは毎 cron 同じ最後尾で、溢れだけを止めても「結果を決めるのは loop 上の位置」という元の欠陥の形がそのまま残るため。
rotateReposForRunが tick ごとに走査順をずらす。offset はscheduledTimeから導出するので watermark も schema 変更も不要で、同じ tick に対しては決定的(= テスト可能)。取り分の値について(レビュー時に見てほしい点)
定数は call site から読み取った会計単位であり、Cloudflare の課金単位ではない。この会計では、失敗した run は 6 番目の repo が例外を投げた時点で消費約 300 であり、文書化された上限 1000 には届いていない。差は poller 側から見えるもので説明できないため、少なくとも 1 つの binding がこの会計の 1 より高く課金されている(どれが・どれだけかは未計測)。
したがって取り分は、唯一手元にある硬い観測 —— その run はこの単位で 300 未満で枯渇した —— に対して較正し 250 とした。文書化された 1000 に合わせると、この単位では到達不能な値になり、上限が永久に発火しない(= 修正にならない)。
DIFF_SUBREQUEST_BUDGET_PER_RUNが 900 なのは diff surface の per-file 見積もりが実測された reject に対して導出されているからで、この surface にはその導出が無い以上、その数値の確度は借りていない。低く取る代償は latency で、最も安い場所で支払われる: 先送りされた repo は cursor から再開し、費用の高い pass は元々複数 run に跨がる bulk import であり、索引済みの wiki は hash 比較で止まって embed の fan-out に到達しない(定常状態では全 repo が満額歩く)。
poller.tsと docs に残っていたinvocation budget of 1000を前提とした安全性の主張も、宣言した取り分を参照する形に直した。テスト
src/poller-wiki.test.tsに 10 件追加(合計 36 件、全体 290 + 70 件 pass)。Too many subrequestsがログに一切現れない。空振り防止のため「3 つ以上の repo が実際に歩かれたこと」も表明しているinconclusive、404 を観測した miss はfailed(両方向)CI 相当をローカルで全て通過:
npm test/npx tsc --noEmit/node scripts/check-schema-drift.mjs/npx wrangler deploy --dry-run。要件記述
docs/0-requirements.md/docs/0-requirements.ja.mdを同一 PR で更新。