Skip to content

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

Description

@smileygames

purpose

wiki surface には repo 単位の予算しか無く、run 全体の subrequest 予算が存在しない。diff surface には DIFF_SUBREQUEST_BUDGET_PER_RUN = 900poller.ts:203)があるが、wiki 側に対応するものがない。結果として、1 invocation で複数リポジトリを歩くうちに Worker の subrequest 上限に到達し、最後に処理されるリポジトリが枯渇を一身に受ける

observation

2026-08-15T03:45:17Z の 45 * * * * tick を wrangler tail で直接観測した。

処理順と結果:

# repo 結果
1 github-webhook-mcp 5 pages, 5 visited, 5/20 fetches, 0 failed
2 github-rag-mcp 7 pages, 7 visited, 7/20 fetches, 0 failed
3 liplus-desktop not enabled — skip
4 liplus-language 87 pages, 20 visited, 20/20 fetches, 0 failed(予算上限に到達しカーソル退避、正常)
5 dipper_ai 5 pages, 5 visited, 5/20 fetches, 0 failed
6 neuron-graph-rag 12 pages, 10 visited, 20/20 fetches, 8 failed

最後の 1 つだけが失敗している。失敗形は fetchWikiContent 内の fetch() が投げる例外である。

(error) fetchWikiContent probe md failed for Liplus-Project/neuron-graph-rag/observation-lifecycle-test-exception:
        Too many subrequests by single Worker invocation.
(error) fetchWikiContent probe markdown failed for ... (同上)
(warn)  No content fetched for .../observation-lifecycle-test-exception (all candidates 404)

WIKI_EXTENSIONS = ["md", "markdown"]poller.ts:1260)を両方叩いて両方が例外になっている。すなわち wiki 自身の 20 予算ではなく、invocation 全体の上限に当たっている。poller.ts:1960 のコメントが前提として置いている an invocation budget of 1000 がそれである。

なお No content fetched ... (all candidates 404) は誤誘導的なメッセージで、実際は 404 ではなく例外である。この文言自体も直す価値がある。

予算構造

poller.ts に現存する wiki 予算はすべて repo 単位である。

  • MAX_WIKI_FETCHES_PER_REPO_PER_RUN = 20(1283)
  • MAX_WIKI_EMBEDDINGS_PER_RUN = 30(1265)
  • MAX_WIKI_DELETIONS_PER_REPO_PER_RUN = 5(1290)
  • MAX_WIKI_REAP_PROBES_PER_REPO_PER_RUN = 15(1307、1 件あたり 3 subrequest に fan out する旨が 128 に記載)

各 repo は個別には規約を守る。総和を見る主体がいないことが欠陥である。

正しい形

diff surface と同じく、run 全体の予算を置き、repo をまたいで減算する。

残予算 = WIKI_SUBREQUEST_BUDGET_PER_RUN - これまでの全 repo の消費
各 repo の fetchBudget = min(MAX_WIKI_FETCHES_PER_REPO_PER_RUN, 残予算)

残予算が尽きた時点で以降の repo はカーソルを進めずに次 run へ送る。liplus-language が示している「予算上限に達したらカーソルを残して正常終了」の挙動を、run 全体の軸にも適用する形になる。

constraints

  • 既存の repo 単位予算は維持する。run 全体の予算は上から被せる形で、置き換えではない
  • 枯渇時に failed を記録してはならない。実体の有無を観測できていないため、liplus-language と同じくカーソル退避で次 run に送ること。現状は観測していない失敗を記録している
  • リポジトリの処理順を固定したままだと、常に最後尾が痩せる。 順序の公平性(run ごとのローテーション等)を入れるかは判断すること。入れない場合、その判断理由を記録すること
  • poller.ts:1960invocation budget of 1000 はコメント内の前提値である。実際の予算定数をどこに置くかは実装側の判断とする
  • No content fetched ... (all candidates 404) の文言を、例外と本物の 404 が区別できる形に直す
  • 要件記述(docs/0-requirements.md / docs/0-requirements.ja.md)を同一 PR で更新する

target files

  • src/poller.ts — wiki 予算定数群(1260-1307)/ fetchWikiContent(1633)/ 予算消費側(1846-1981)/ 複数 repo を歩く呼び出し元
  • src/poller-wiki.test.ts
  • docs/0-requirements.md / docs/0-requirements.ja.md

acceptance

実装中に前提が 1 つ崩れたので、該当項目を実際に達成可能な形へ改訂した(改訂理由は下の「予算単位についての訂正」を参照)。

  • 複数リポジトリを 1 run で歩いても Too many subrequests by single Worker invocation が出ない
    改訂: 最初の 1 回の拒否は防げない(それが検知イベントそのもの)。達成すべきは、1 回の拒否が「最後尾リポジトリにおける偽の page failure の連鎖」に変わらないこと。枯渇を検知した時点で run を終了し、probe 中だったページは cursor 未前進、reap は省略、残りのリポジトリは未接触で先送りする
  • 枯渇時は failed ではなくカーソル退避で終わり、次 run で同じページから再開する
  • 上記 6 リポジトリの構成を模した fixture で、最後尾リポジトリが 0 failed になる回帰テスト
  • 追加: 上記 fixture を未変更ページ(定常運転)のプロファイルで走らせる回帰テスト。変更済みページだけの fixture は本番と費用プロファイルが逆になり、run 全体の予算が発火しないケースを取り逃がす
  • run 全体の予算が repo 単位予算より優先されることの回帰テスト
  • liplus-language の一巡挙動(87 ページを複数 run で消化)が破綻していない

予算単位についての訂正

本 issue は「run 全体の予算を置けば直る」という前提で書かれたが、実装中にその予算を較正できないことが判明した。

poller.ts の定数は call site を数えた会計単位(fetch 1 回・DO 往復 1 回・D1 statement 1 本・Vectorize / Workers AI 呼び出し 1 回をそれぞれ 1)であり、Cloudflare の課金単位ではない。この単位で観測された run を再計算すると、6 番目のリポジトリが例外を投げた時点の消費は 約 70〜292(全ページ未変更 〜 全ページ再 embed の幅)であって、文書化された上限 1000 には遠い。差は poller 側から見えるもので説明できず、Workers は実行時に消費量を数える手段を提供していない。したがってこのファイルのどの定数も実際の上限に対して較正できない。

さらに、上の幅は計測誤差ではなく費用プロファイルそのものである。未変更ページは embed fan-out が hash 比較の後で加算されるため fetch 試行分しか課金されない。定常運転(= 毎時 cron の通常の姿であり、実際に例外を投げたときの姿)では、run 全体の予算は発火せず、run は失敗したときと同じ順序で進む。

よって修正は 2 軸に分かれる:

  • run 全体の予算 = bulk import の形(fetch したページごとに embed fan-out が乗る)を縛る。観測された事象への対処ではない。値は観測幅の中での判断であって較正ではない。この否定的な結果自体を回帰テストで固定した
  • 枯渇の実行時検知 = 観測された事象への対処。上限到達は見積もりではなく観測できる(binding が例外を投げ、名前を名乗る)。枯渇は invocation スコープかつ終端的なので、最初の検知で run を終了する

順序の公平性(rotation)は入れた。固定順のままだと痩せるのは毎 cron 同じ最後尾で、溢れだけを止めても「結果を決めるのは loop 上の位置」という元の欠陥の形が残るため。

経緯

#246 の本番検証で wrangler tail を張った際に副次的に観測した。#246(diff の embed batch 境界)とは別 invocation・別予算であり、因果関係はない。同 tick の diff surface は Failed to batch-embed ゼロを確認済み。

本 issue は当初「予算が subrequest ではなくページ単位で数えられている」という診断で起票されたが、これは誤りだった。poller.ts:1967fetches += attempts が示す通り、repo 単位の予算はすでに subrequest 単位で数えられている。欠けているのは run 全体の軸である。

Metadata

Metadata

Assignees

Labels

bug動いていない、壊れているready本文が実装開始できる形まで収束している状態。ただし更新は継続可能

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions