Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion docs/0-requirements.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,17 @@ wiki poller は `:45` cron 専属で、GitHub Wiki content の唯一の取り込

**カバレッジ.** page 走査の上限は `MAX_WIKI_FETCHES_PER_REPO_PER_RUN`(既定 20)。page 単位ではなく **HTTP 試行単位**で数えるので、候補を複数持つ page が fan-out で上限を超えることはない。この予算は深い wiki より小さいため、走査は**循環し、保存された cursor から再開する** — 最後に probe した slug を `wiki:{repo}` watermark 行の `etag` 列に置くので、schema 変更は不要。結果として全 page が ceil(pages / 予算) run 以内に到達する。毎 run 列挙の先頭から舐め直す実装が、位置 20 以降を構造的に到達不能にし、77 page の wiki の大半を未索引のまま放置していた(issue #184)。`MAX_WIKI_EMBEDDINGS_PER_RUN`(既定 30)は別軸のまま — Workers AI の embed 予算の上限であって、走査の上限ではない。

**先頭 page では予算より候補リストを優先する.** 予算切れで打ち切った probe は「404 を観測した」ことにはならないので、walk は cursor を進める**手前**で break し、その page を次の pass に持ち越す(issue #185)。これが自己修復するのは、次の pass がその page の候補リストを最後まで払える場合だけで、予算が 1 page の候補数を下回ると同じ page を永久に probe し直し、walk は前進しない(issue #192)。そこで、その pass でまだ 1 page も visit していない間は候補リストを予算より優先する — 先頭 page は必ず候補を試し切り、そこでの miss は候補を全て観測した上での 404 なので failure として計上し cursor も前進させる。2 page 目以降は従来どおり厳密に予算で打ち切る。超過は 1 page の候補数 - 1 が上限(最大 3 subrequest)で、pass ごとに 1 回だけ。invocation あたり 1000 の予算に対して無視できる範囲であり、pass の `fetches` が予算を超えうる唯一のケースでもある。
**run 全体の予算.** 上に挙げた予算はすべて *repo 単位* である。wiki cron は 1 invocation の中で `POLL_REPOS` を全て歩くので、各 repo は個別には規約を守る一方、その総和を見る主体がいなかった — そして Cloudflare が課金するのは総和のほうである。2026-08-15T03:45Z の tick で観測: 5 つの repo は正常に索引され、6 つ目は raw content probe が全て `Too many subrequests by single Worker invocation` で弾かれ、誰も見ていない page について 8 件の failure を記録した。枯渇は loop が最後に到達した repo が一身に受ける。これは loop の性質であって、その repo の性質ではない。そこで `WIKI_SUBREQUEST_BUDGET_PER_RUN` が invocation に対するこの surface の取り分を宣言し、`wikiFetchBudgetForPass` がその残りを pass の fetch 上限へ変換し、`runWikiSurfaces` が各 pass の *実測* 消費を差し引いてから次の repo を通す。除算ではなく減算である — diff surface の `diffFileBudgetPerPhase` が除算なのは phase の費用が repo ごとに事前に確定するからで、wiki の走査費用は走らせてみるまで分からない。おかげで変更が無かった repo は取り分をほぼ丸ごと後続へ渡す。残予算が pass を賄えなくなった時点で、以降の repo は**呼び出し自体を行わない** — cursor は前 run が残した位置のまま据え置かれ、次の invocation がそこから再開する。深い wiki 1 つが repo 単位の fetch 予算に対して既に持っていた「上限に達したら cursor を残して正常終了」という挙動を、run 全体の軸へ持ち上げた形である。reap の予算は消費した分を全額計上する一方、admission では意図的に確保 *しない* — 取り分 250 に対して repo ごとの最悪値 50 を先取りすると、reap がほとんど何もしない通常時にも全 pass へ課税することになり、何も起きていない run で repo を先送りしてしまう。

**この会計単位は Cloudflare の課金単位ではなく、取り分だけでは観測された事象を直せない.** 定数は call site から読み取ったもので、`fetch` 1 回・Durable Object 往復 1 回・D1 statement 1 本・Vectorize 呼び出し 1 回・Workers AI 呼び出し 1 回をそれぞれ 1 として数えている。ここから 2 つのことが従う。重要なのは 2 つ目である。1 つ目は差の存在: この単位では、6 つ目の repo の最初の probe が例外を投げた時点の消費は約 70(全 page 未変更の場合)から約 292(全 page 再 embed の場合)の間であり、文書化された上限 1000 には遠い。poller 側から見えるものでこの差は説明できず、少なくとも 1 つの binding がこの会計の 1 より高く課金されている。Workers は実行時に消費量を数える手段を提供していないので、**このファイルのどの定数も実際の上限に対して較正できない**。2 つ目は、上の幅が計測誤差ではなく費用プロファイルそのものだという点: 未変更の page は embed fan-out が hash 比較の後で加算されるため fetch 試行分しか課金されない。つまり索引済みの wiki に対する定常運転の run は、同じ page 数の bulk import に比べてごく僅かしか消費しない。**定常状態のプロファイル —— 毎時 cron の通常の姿であり、実際に例外を投げたときの姿 —— では、取り分 250 は何も先送りせず、run は失敗したときと全く同じ順序で進む。** したがって取り分が縛るのは bulk import の形(fetch した page ごとに embed fan-out が乗るため、数 repo で 250 に達する)であって、報告された failure に対処する機構ではない。`DIFF_SUBREQUEST_BUDGET_PER_RUN` が 900 なのは diff surface の per-file 見積もりが実測された reject に対して導出されているからで、この surface にはその導出が無い以上、値は観測された幅の中での判断であって較正ではない。この否定的な結果は回帰テストで固定してあり、黙って成り立たなくなることはない。

**では何が対処しているか: 壁は観測できる.** 上に挙げた予算はすべて見積もりだが、上限に到達したこと自体は見積もりではない — binding が例外を投げ、しかも自分の名前を名乗る。これは推測ではなく計測である。枯渇は invocation スコープかつ終端的で、1 つの subrequest が拒否された後は同じ invocation の後続も全て拒否される。観測された run が probe ごとに例外を投げ続け、walk がそれを page の failure として記録し続けたのはそのためである。よって最初に検知した時点で run を終了する。probe 中だった page は未観測のまま cursor を動かさず、reap は丸ごと省略し(その probe も同じ壁に当たるうえ、候補集合は縮む一方なので見送りに costs は無い)、残りの repo は全て未接触で先送りする。これで**できない**のは *最初の* 拒否を防ぐことで、それが検知イベントそのものだからである。したがって「ログに `Too many subrequests` が出ない」という字義どおりの達成は、binding ごとの実費用を知らない限り不可能である。達成されるのは、1 回の拒否が「loop が最後に到達した repo における偽の page failure の連鎖」に変わらなくなることである。

**処理順は run ごとに回転する.** run 全体の予算を list 順に消費すると、痩せるのは毎 cron 同じ最後尾である — 溢れだけを止めた元の欠陥そのもので、結果を決めるのは依然として loop 上の位置になる。`rotateReposForRun` が tick によって走査順をずらすので、毎時 cron と N repo なら各 repo は N 時間に 1 度先頭に立ち、予算枯渇で先送りされた repo は後続の tick で早い位置に回る。offset は保存せず cron の `scheduledTime` から導出するため watermark を必要とせず、同じ tick に対しては決定的である。

**観測した miss と観測していない miss は別のカウンタ.** 候補が全て 404 を返した page は観測であり `failed` に計上する。probe が *例外を投げた* page は観測ではなく — subrequest 枯渇は例外を投げる — `inconclusive` に計上する。両者は "all candidates 404" という同一の文言で報告されており、実在する 8 つの page が「無い」と読めたのはそのためである。`inconclusive` でも cursor は前進させる。その page は次の周回で再試行される一方、cursor を留めると恒久的に到達できない page 1 つが walk 全体を停止させうるからである。

**先頭 page では予算より候補リストを優先する.** 予算切れで打ち切った probe は「404 を観測した」ことにはならないので、walk は cursor を進める**手前**で break し、その page を次の pass に持ち越す(issue #185)。これが自己修復するのは、次の pass がその page の候補リストを最後まで払える場合だけで、予算が 1 page の候補数を下回ると同じ page を永久に probe し直し、walk は前進しない(issue #192)。そこで、その pass でまだ 1 page も visit していない間は候補リストを予算より優先する — 先頭 page は必ず候補を試し切り、そこでの miss は候補を全て観測した上での 404 なので failure として計上し cursor も前進させる。2 page 目以降は従来どおり厳密に予算で打ち切る。超過は 1 page の候補数 - 1 が上限(最大 3 subrequest)で、pass ごとに 1 回だけ。pass の `fetches` が予算を超えうる唯一のケースでもある。これを吸収するのは、この文がかつて引き合いに出していた文書上の invocation 上限ではなく、後述の run 全体の取り分が持つ余裕である。そもそも取り分が 1 page 分を賄えない pass は admission されない。

**周回の完了.** 2 本目の watermark 行 `wiki-lap:{repo}` が *lap anchor* — 現在の周回がどの slug の次から始まったか — を保持する。pass は anchor の直前の page に到達した時点、つまり cursor が一周して戻ってきた時点で `wrapped: true` を返し、anchor はその page へ移動して次の周回がその次から始まる。これが admin endpoint の `done` を到達可能にしている。「この 1 回の pass で全 page を踏破した」という意味では、1 pass の fetch 予算より page 数が多い wiki で真になりようがなく、「`done` まで呼び続けろ」という手順に停止条件が無かった(issue #188)。anchor は保存した index ではなく slug 順で解決するので、周回の途中で page が増減しても desync しない。`cursor=` を明示指定した場合はその地点から新しい周回を開始する。

Expand Down
Loading
Loading