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 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-03 20:48:13 +02:00
parent 41bf9455a1
commit 6f0a8d88c9
3 changed files with 32 additions and 0 deletions

View File

@@ -297,6 +297,7 @@ async fn list_dead_jobs(
enum RequeueRequest { enum RequeueRequest {
All, All,
Manga { manga_id: Uuid }, Manga { manga_id: Uuid },
Chapter { chapter_id: Uuid },
Job { job_id: Uuid }, Job { job_id: Uuid },
} }
@@ -313,6 +314,7 @@ async fn requeue_dead_jobs(
let scope = match &body { let scope = match &body {
RequeueRequest::All => RequeueScope::All, RequeueRequest::All => RequeueScope::All,
RequeueRequest::Manga { manga_id } => RequeueScope::Manga(*manga_id), RequeueRequest::Manga { manga_id } => RequeueScope::Manga(*manga_id),
RequeueRequest::Chapter { chapter_id } => RequeueScope::Chapter(*chapter_id),
RequeueRequest::Job { job_id } => RequeueScope::Job(*job_id), RequeueRequest::Job { job_id } => RequeueScope::Job(*job_id),
}; };
let requeued = repo::crawler::requeue_dead_jobs(&state.db, scope).await?; let requeued = repo::crawler::requeue_dead_jobs(&state.db, scope).await?;
@@ -332,6 +334,7 @@ fn scope_label(r: &RequeueRequest) -> &'static str {
match r { match r {
RequeueRequest::All => "all", RequeueRequest::All => "all",
RequeueRequest::Manga { .. } => "manga", RequeueRequest::Manga { .. } => "manga",
RequeueRequest::Chapter { .. } => "chapter",
RequeueRequest::Job { .. } => "job", RequeueRequest::Job { .. } => "job",
} }
} }

View File

@@ -706,6 +706,8 @@ pub enum RequeueScope {
All, All,
/// Dead jobs whose chapter belongs to this manga. /// Dead jobs whose chapter belongs to this manga.
Manga(Uuid), Manga(Uuid),
/// Dead jobs for a single chapter.
Chapter(Uuid),
/// A single dead job by its id. /// A single dead job by its id.
Job(Uuid), Job(Uuid),
} }
@@ -751,6 +753,18 @@ pub async fn requeue_dead_jobs(pool: &PgPool, scope: RequeueScope) -> sqlx::Resu
.await? .await?
.rows_affected() .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) => { RequeueScope::Job(job_id) => {
sqlx::query(&format!( sqlx::query(&format!(
"UPDATE crawler_jobs {SET} WHERE state = 'dead' AND id = $1 {NO_LIVE_DUP}" "UPDATE crawler_jobs {SET} WHERE state = 'dead' AND id = $1 {NO_LIVE_DUP}"

View File

@@ -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"); 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")] #[sqlx::test(migrations = "./migrations")]
async fn requeue_single_job(pool: PgPool) { async fn requeue_single_job(pool: PgPool) {
let (_m, c1) = seed_chapter(&pool, "A", 1).await; let (_m, c1) = seed_chapter(&pool, "A", 1).await;