fix(correctness): pagination tiebreakers, dedup race, duration gating (0.87.6)
Three medium correctness fixes: 1. **Pagination ORDER BY missing tiebreakers** — four admin list queries sorted by a timestamp or `chapters.number` with no stable secondary sort. Add `id` tiebreakers across `repo::admin_view`, `repo::admin_audit`, `repo::bookmark`. 2. **`enqueue_pages` NOT EXISTS read-then-insert race** — concurrent admin clicks could land duplicate analyze_page jobs. Migration 0031 adds a partial unique index mirroring 0014; query relies on `ON CONFLICT DO NOTHING`. 3. **`record_duration` overwrote a prior done row's duration on a non-terminal failed retry of a force-re-analyze.** Gate the call on "did this attempt actually write a row?". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,14 @@ pub async fn enqueue_pages(
|
||||
}
|
||||
ReenqueueScope::Chapter(_) => "AND p.chapter_id = $2",
|
||||
};
|
||||
// The `NOT EXISTS(... pending|running ...)` pre-check used to live in
|
||||
// this query, but it races with concurrent admin clicks (two requests
|
||||
// both see "no in-flight job" and both INSERT). The partial unique
|
||||
// index `crawler_jobs_analyze_page_dedup_idx` (migration 0031) covers
|
||||
// (payload->>'page_id') WHERE state IN ('pending', 'running') AND
|
||||
// kind = 'analyze_page', so `ON CONFLICT DO NOTHING` is now the
|
||||
// atomic primitive — duplicates can't land and concurrent enqueue
|
||||
// calls are both observably correct.
|
||||
let sql = format!(
|
||||
r#"
|
||||
INSERT INTO crawler_jobs (payload)
|
||||
@@ -95,11 +103,7 @@ pub async fn enqueue_pages(
|
||||
SELECT 1 FROM page_analysis pa
|
||||
WHERE pa.page_id = p.id AND pa.status = 'done'))
|
||||
{scope_clause}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM crawler_jobs j
|
||||
WHERE j.payload->>'kind' = 'analyze_page'
|
||||
AND j.payload->>'page_id' = p.id::text
|
||||
AND j.state IN ('pending', 'running'))
|
||||
ON CONFLICT DO NOTHING
|
||||
"#
|
||||
);
|
||||
let query = sqlx::query(&sql).bind(only_unanalyzed);
|
||||
|
||||
Reference in New Issue
Block a user