Skip to content

fix(webhook): tear down push-path doc delete surfaces independently [webhook, docs, tests] - #207

Merged
liplus-lin-lay merged 1 commit into
mainfrom
206-bugwebhook-the-push-path-doc-delete-lets-a-vectorize-failure-strand-the-store-row
Aug 2, 2026
Merged

fix(webhook): tear down push-path doc delete surfaces independently [webhook, docs, tests]#207
liplus-lin-lay merged 1 commit into
mainfrom
206-bugwebhook-the-push-path-doc-delete-lets-a-vectorize-failure-strand-the-store-row

Conversation

@liplus-lin-lay

Copy link
Copy Markdown
Member

Closes #206

何が壊れていたか

src/webhook.ts の doc 削除ループは、1 件を 1 本の外側 try で包んでいた。Vectorize の deleteByIds が throw すると、そのまま catch に飛ぶので FTS5 行の削除も store 行の DELETE も一度も実行されない。削除したはずの doc が search に出続ける。実際に retrieval されるのは D1 側の行なので、影響が出るのはここ。

同ファイルの他の削除経路(issue/PR、release、comment、review、review comment)は既に surface ごとに try が分かれていた。doc 経路だけが取り残されていて、しかも push は一次経路で cron はその fallback なので、実際に多く通るのはこちら。

issue 本文の記述の訂正

issue 本文は「Vectorize 失敗時に FTS5 だけ消えて store 行が残る」としていたが、実測はそうではなかった。内側の FTS5 try は Vectorize 呼び出しの後ろにあるので、Vectorize が throw した時点で FTS5 にも store にも到達しない。取り残しの範囲は本文の記述より広い。欠陥そのもの(Vectorize の失敗が D1 側の teardown を丸ごと飛ばす)は成立するので、修正内容は変えていない。

pre-fix の実装に対して新テストを走らせて確認した結果:

  • Vectorize 失敗 → deleteFtsRow の呼び出し回数が 0(本文の記述どおりなら 1 のはず)
  • FTS5 失敗 → 内側 try が握り潰すので store DELETE は到達していた(ここは本文どおり)

変更

3 面(Vectorize / D1 FTS5 / store 行)をそれぞれ独立した try で teardown する形に変えた。#205 で cron 側(src/poller.ts)が取った形をそのまま持ってきている。

deleted カウンタの意味 — cron に揃えなかった理由

issue のコメントで「cron 側(試行数を数える)に揃えるのが素直、揃えないなら理由を PR 本文に」とあったので、揃えなかった理由を書く。

採った意味: deleted = 3 面すべての teardown が成功した doc の件数。 したがって removed - deleted が部分失敗の件数になる。

cron 側の removedDocs が試行数を数えているのは、あれが MAX_DOC_DELETIONS_PER_REPO_PER_RUN の cap を駆動し、さらに ETag hold を決める budget counter を兼ねているから。予算を消費するのは試行であって成功ではないので、あちらでは試行数が正しい。

webhook 側には budget が無い。ここで試行数を数えると、cap が無い以上ループは removed 全件を回るので deleted は常に removed.size と一致するremoved は同じレスポンス body にすでに入っているので、それは同じ数字を 2 回書いているだけになる。しかも部分失敗が body から完全に消え、console.error を wrangler tail で拾いに行かないと分からなくなる。

webhook のレスポンスは GitHub の delivery log に残る、運用者が最も安く見られる観測面なので、そこに信号を残す側を採った。カウンタの職掌が両者で違う(budget counter か、観測用フィールドか)ので、意味が分かれるのはむしろ構造どおりだと判断している。

テスト

src/webhook-push-docs.test.ts を新設(6 ケース)。deleteFtsRow を mock、Vectorize と Store DO は in-memory の stand-in。commit に id を持たせないことで diff indexing 分岐を short-circuit させ、global fetch を stub せずに済ませている(HTTP を打ちに行く変更が入ったら落ちる)。

  • 3 面すべて成功する通常ケース(vector ID が 3 面で一致すること、deleted: 1
  • Vectorize 失敗でも FTS5 行と store 行の teardown が実行される(受け入れ条件。deleted: 0 も固定)
  • FTS5 失敗でも store 行の teardown が実行される
  • store DELETE 失敗でも他 2 面の teardown が実行される
  • 1 件が失敗しても後続の doc の reap が続く(removed: 2, deleted: 1
  • 削除対象が無い push では reap が空回りしない

pre-fix 実装に対する検証: export だけ足して修正前のループに戻し、同じテストを走らせて 3 ケースが落ちることを確認済み(vacuous test でないことの確認)。

docs

docs/0-requirements.md / .ja.md の Webhook Receiver 節に、push 経路の doc 削除の 3 面 teardown・cap を持たない理由・deleted の意味を追記した。cron 側の記述(#205 で追加)と対になる位置。

検証

  • npx tsc --noEmit パス
  • npx vitest run — 12 files / 131 tests パス
  • npx vitest run --config vitest.workers.config.ts — 4 files / 53 tests パス

…webhook, docs, tests]

The push-path doc delete wrapped one removed file in a single outer try, so a
Vectorize failure jumped to the catch and neither the D1 FTS5 row nor the store
row was ever touched. The deleted doc kept surfacing in search — the D1 rows are
the ones users actually retrieve. Every other delete path in this file already
split its surfaces; docs was the last one left, and it is the primary path
(cron is its fallback), so it is the one that actually runs.

The three surfaces — Vectorize, D1 FTS5, store row — now each carry their own
try, adopting the shape the cron reap took in #205. Graph edges are still not
torn down, same invariant as there: a doc vector ID is never a `doc_edges`
endpoint.

No per-run cap, unlike the cron reap: a push payload names its own removals, so
this loop is bounded by the event rather than by an accumulated backlog.

`deleted` in the response now counts docs whose three surfaces all came down,
which makes `removed - deleted` the partial-teardown count. The cron reap counts
attempts instead; the reason for the divergence is in the PR body.

handlePushEvent is exported for the new test file.

issue 本文の「Vectorize 失敗時に FTS5 だけ消えて store 行が残る」は実測と食い
違っていた。内側 try は Vectorize 呼び出しの後ろにあるので、Vectorize が throw
した時点で FTS5 も store も一度も触られない。取り残しの範囲は本文の記述より広
かったが、欠陥そのもの(Vectorize の失敗が D1 側の teardown を丸ごと飛ばす)は
そのまま成立する。

Closes #206
@cloudflare-workers-and-pages

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 d30b5b7 Aug 02 2026, 12:30 PM

@liplus-lin-lay liplus-lin-lay 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.

Self-review (auto mode, parent agent)

issue 本文の記述誤りを訂正して受理する

本文は「Vectorize 失敗時に FTS5 だけ消えて store 行が残る」と書いていたが、誤りdeleteFtsRow の内側 try は VECTORIZE.deleteByIds後ろにあるため、Vectorize が throw した時点で外側 catch に飛び、D1 の 2 面はどちらも触られない。実装者が pre-fix 実装(export のみ追加)に対して測り直し、deleteFtsRow の呼び出し回数が 1 ではなく 0 であることを実測で確認している。

欠陥の所在と修正内容は変わらないが、取り残される範囲は本文の記述より広い。訂正は PR 本文と commit message に入っている。issue #206 の本文はこちらで直す。

これは私が本セッションで 3 度目に踏んだ形(#202 継承前提の無検証再掲 / #203 コードの差からデータ欠陥を推論 / 本件 コードを読んだうえで制御フローを gist で記述)。tally に記録済み。

受け入れ条件

  • Vectorize 失敗でも FTS5 行と store 行の teardown が実行される — 満たす。3 面を独立 try に分離。pre-fix 実装に対し 6 本中 3 本が fail することを実測確認済みで、テストが空振りでないことの裏取りがある
  • 他の webhook 削除経路に変更が無い — 満たす。変更は doc 削除ループのみ
  • Vectorize 失敗時の deleted の値がテストで固定されている — 満たす(removed: 1, deleted: 0

deleted counter を cron と揃えなかった判断 — 受理する

追記で「揃えないなら理由を残せ」と要求した点。理由が構造的で妥当なので受理する。

cron 側の removedDocs が試行数を数えるのは、その counter が MAX_DOC_DELETIONS_PER_REPO_PER_RUN と ETag hold を制御する budget counter を兼ねているため。予算を消費するのは試行であって成功ではない。

webhook 側には予算も cap も無い。試行数を数えると deleted は常に removed と一致し、レスポンス body は既に docs: { removed, deleted, failed } の形で removed を持っているので、同じ数を二度書くだけになり、部分失敗が delivery log から消えるconsole.error にしか残らない)。

counter が両経路で違う仕事をしている以上、値の意味が違うのは構造的な差であって不揃いではない。観測可能性を取る側が正しい。

scope

handlePushEvent の export はテストのためで、#205pollDocs export と同じ扱い。docs は cron 側の段落と対にする形で Webhook Receiver 節に追加されており、両経路の差(graph edge を消さない理由 / cap が無い理由 / counter の意味の違い)がすべて書かれている。撤回済みの edge 論拠が混入していないことを確認した。

brake

user repo の PR で Li+ source を触っていないため brake 1 / brake 2 の対象外。

次のステップ

auto mode につき human gate なし。self-review pass → merge。

@liplus-lin-lay
liplus-lin-lay merged commit f196d4b into main Aug 2, 2026
3 checks passed
@liplus-lin-lay
liplus-lin-lay deleted the 206-bugwebhook-the-push-path-doc-delete-lets-a-vectorize-failure-strand-the-store-row branch August 2, 2026 12:33
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.

bug(webhook): the push-path doc delete lets a Vectorize failure strand the store row

1 participant