From 6f0a8d88c9ef64f2d21d32e3849975cc5d96377c Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Wed, 3 Jun 2026 20:48:13 +0200 Subject: [PATCH] feat(api): add per-chapter requeue scope for dead jobs Lets the admin manga page requeue a single failed chapter's dead job(s) inline, without a job id. Adds RequeueScope::Chapter + the matching request variant and a repo test. Co-Authored-By: Claude Opus 4.8 --- backend/src/api/admin/crawler.rs | 3 +++ backend/src/repo/crawler.rs | 14 ++++++++++++++ backend/tests/crawler_dead_jobs.rs | 15 +++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/backend/src/api/admin/crawler.rs b/backend/src/api/admin/crawler.rs index 8529fbd..d9d445d 100644 --- a/backend/src/api/admin/crawler.rs +++ b/backend/src/api/admin/crawler.rs @@ -297,6 +297,7 @@ async fn list_dead_jobs( enum RequeueRequest { All, Manga { manga_id: Uuid }, + Chapter { chapter_id: Uuid }, Job { job_id: Uuid }, } @@ -313,6 +314,7 @@ async fn requeue_dead_jobs( let scope = match &body { RequeueRequest::All => RequeueScope::All, RequeueRequest::Manga { manga_id } => RequeueScope::Manga(*manga_id), + RequeueRequest::Chapter { chapter_id } => RequeueScope::Chapter(*chapter_id), RequeueRequest::Job { job_id } => RequeueScope::Job(*job_id), }; let requeued = repo::crawler::requeue_dead_jobs(&state.db, scope).await?; @@ -332,6 +334,7 @@ fn scope_label(r: &RequeueRequest) -> &'static str { match r { RequeueRequest::All => "all", RequeueRequest::Manga { .. } => "manga", + RequeueRequest::Chapter { .. } => "chapter", RequeueRequest::Job { .. } => "job", } } diff --git a/backend/src/repo/crawler.rs b/backend/src/repo/crawler.rs index 2560b1b..86f032c 100644 --- a/backend/src/repo/crawler.rs +++ b/backend/src/repo/crawler.rs @@ -706,6 +706,8 @@ pub enum RequeueScope { All, /// Dead jobs whose chapter belongs to this manga. Manga(Uuid), + /// Dead jobs for a single chapter. + Chapter(Uuid), /// A single dead job by its id. Job(Uuid), } @@ -751,6 +753,18 @@ pub async fn requeue_dead_jobs(pool: &PgPool, scope: RequeueScope) -> sqlx::Resu .await? .rows_affected() } + RequeueScope::Chapter(chapter_id) => { + sqlx::query(&format!( + "UPDATE crawler_jobs {SET} \ + WHERE state = 'dead' \ + AND (payload->>'chapter_id')::uuid = $1 \ + {NO_LIVE_DUP}" + )) + .bind(chapter_id) + .execute(pool) + .await? + .rows_affected() + } RequeueScope::Job(job_id) => { sqlx::query(&format!( "UPDATE crawler_jobs {SET} WHERE state = 'dead' AND id = $1 {NO_LIVE_DUP}" diff --git a/backend/tests/crawler_dead_jobs.rs b/backend/tests/crawler_dead_jobs.rs index 7165ca3..3642728 100644 --- a/backend/tests/crawler_dead_jobs.rs +++ b/backend/tests/crawler_dead_jobs.rs @@ -126,6 +126,21 @@ async fn requeue_by_manga_scopes_to_that_manga(pool: PgPool) { assert_eq!(state_of(&pool, j2).await, "dead", "other manga untouched"); } +#[sqlx::test(migrations = "./migrations")] +async fn requeue_by_chapter_scopes_to_that_chapter(pool: PgPool) { + let (_m, c1) = seed_chapter(&pool, "A", 1).await; + let (_m2, c2) = seed_chapter(&pool, "A", 2).await; + let j1 = insert_job(&pool, c1, "dead", 5).await; + let j2 = insert_job(&pool, c2, "dead", 5).await; + + let n = crawler::requeue_dead_jobs(&pool, RequeueScope::Chapter(c1)) + .await + .unwrap(); + assert_eq!(n, 1); + assert_eq!(state_of(&pool, j1).await, "pending"); + assert_eq!(state_of(&pool, j2).await, "dead", "other chapter untouched"); +} + #[sqlx::test(migrations = "./migrations")] async fn requeue_single_job(pool: PgPool) { let (_m, c1) = seed_chapter(&pool, "A", 1).await;