Skip to content

fix(poller): add a run-wide subrequest budget to the wiki surface [poller, docs, tests] - #249

Merged
smileygames merged 4 commits into
mainfrom
248-fix-poller-add-a-run-wide-subrequest-budget-to-the-wiki-surface
Aug 15, 2026
Merged

fix(poller): add a run-wide subrequest budget to the wiki surface [poller, docs, tests]#249
smileygames merged 4 commits into
mainfrom
248-fix-poller-add-a-run-wide-subrequest-budget-to-the-wiki-surface

Conversation

@smileygames

Copy link
Copy Markdown
Member

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 の分離. WikiPollSummarysubrequestsinconclusive を追加。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)。

  • 本番と同じ 6 repo 構成(page 数 5 / 7 / wiki 無効 / 87 / 5 / 12)の fixture で、どの repo も failure を記録しない。歩かれなかった repo は cursor 未変更であることを確認。Too many subrequests がログに一切現れない。空振り防止のため「3 つ以上の repo が実際に歩かれたこと」も表明している
  • run 全体の予算が repo 単位予算より優先される(残予算が薄いと fetch 上限が repo 単位定数を下回る)
  • 予算を賄えない repo は呼び出されず、cursor が据え置かれる。先送りは常に走査順の末尾側で、途中に穴が空かない
  • 先送りされた repo が後続の tick で先頭側に回り、1 周以内に必ず歩かれる
  • 例外による miss は inconclusive、404 を観測した miss は failed(両方向)
  • 87 page の wiki の複数 run 消化が破綻していない(5 run で全 page 到達)
  • 処理順の回転が list を変えず、N tick で全 repo が 1 度ずつ先頭に立つ

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 で更新。

…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
@smileygames smileygames self-assigned this Aug 15, 2026
@smileygames smileygames linked an issue Aug 15, 2026 that may be closed by this pull request
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 15, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
github-rag-mcp 90355f8 Aug 15 2026, 05:59 AM

Claude Lin & Lay added 3 commits August 15, 2026 13:34
…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 smileygames left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 — breaknextCursor = page.slugに置かれている
枯渇時に reap が省略される pass — listIndexedWikiPages の D1 呼び出し自体を回避
枯渇後に後続リポジトリが未接触 pass — hitTheWallcontinuepollWiki を呼ばない
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 の連鎖に変わらないことである。

@smileygames
smileygames merged commit b26086b into main Aug 15, 2026
3 checks passed
@smileygames
smileygames deleted the 248-fix-poller-add-a-run-wide-subrequest-budget-to-the-wiki-surface branch August 15, 2026 11:39
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.

fix(poller): add a run-wide subrequest budget to the wiki surface

1 participant