fix(admin): make force-reanalyze audit transactional; document storage exception
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -368,21 +368,15 @@ async fn reenqueue(
|
||||
(repo::page_analysis::ReenqueueScope::All, "analysis", None)
|
||||
};
|
||||
|
||||
// Enqueue + audit in one transaction so a failed audit insert rolls the
|
||||
// enqueue back too — the audit trail can't silently miss a re-enqueue that
|
||||
// actually landed. Both are plain DB writes, so they share the tx cleanly
|
||||
// (the live event + response are emitted only after commit).
|
||||
let mut tx = state.db.begin().await?;
|
||||
let enqueued =
|
||||
repo::page_analysis::enqueue_pages(&state.db, scope, body.only_unanalyzed).await?;
|
||||
|
||||
// Push a live event so connected dashboards mark the in-scope pages as
|
||||
// queued. Skip the no-op (nothing actually enqueued).
|
||||
if enqueued > 0 {
|
||||
state.analysis_events.publish(AnalysisEvent::Enqueued {
|
||||
count: enqueued,
|
||||
manga_id: body.manga_id,
|
||||
chapter_id: body.chapter_id,
|
||||
});
|
||||
}
|
||||
|
||||
repo::page_analysis::enqueue_pages(&mut *tx, scope, body.only_unanalyzed).await?;
|
||||
repo::admin_audit::insert(
|
||||
&state.db,
|
||||
&mut *tx,
|
||||
admin.0.id,
|
||||
"analysis_reenqueue",
|
||||
target_type,
|
||||
@@ -395,6 +389,18 @@ async fn reenqueue(
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
// Push a live event so connected dashboards mark the in-scope pages as
|
||||
// queued. Skip the no-op (nothing actually enqueued). After commit so a
|
||||
// rolled-back enqueue never emits a phantom event.
|
||||
if enqueued > 0 {
|
||||
state.analysis_events.publish(AnalysisEvent::Enqueued {
|
||||
count: enqueued,
|
||||
manga_id: body.manga_id,
|
||||
chapter_id: body.chapter_id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Json(ReenqueueResponse { enqueued }))
|
||||
}
|
||||
@@ -421,8 +427,13 @@ async fn analyze_page(
|
||||
// when a `force=false` job was already pending — the worker would
|
||||
// then pick up the non-force row, hit skip-if-done, ack done, and
|
||||
// the admin saw "queued for re-analysis" with no re-analysis.
|
||||
//
|
||||
// Enqueue + audit in one transaction (via the `_conn` form) so a failed
|
||||
// audit insert rolls the enqueue/upgrade back too — the audit trail can't
|
||||
// miss a force-reanalyze that landed.
|
||||
let mut tx = state.db.begin().await?;
|
||||
let outcome =
|
||||
repo::page_analysis::enqueue_for_page(&state.db, page_id, true).await?;
|
||||
repo::page_analysis::enqueue_for_page_conn(&mut tx, page_id, true).await?;
|
||||
|
||||
let outcome_label = match outcome {
|
||||
repo::page_analysis::EnqueueForPageOutcome::Inserted => "inserted",
|
||||
@@ -430,7 +441,7 @@ async fn analyze_page(
|
||||
repo::page_analysis::EnqueueForPageOutcome::AlreadyEnqueued => "already_force_enqueued",
|
||||
};
|
||||
repo::admin_audit::insert(
|
||||
&state.db,
|
||||
&mut *tx,
|
||||
admin.0.id,
|
||||
"analysis_force_page",
|
||||
"page",
|
||||
@@ -438,6 +449,7 @@ async fn analyze_page(
|
||||
json!({ "force": true, "outcome": outcome_label }),
|
||||
)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(AnalyzePageResponse { enqueued: true }))
|
||||
}
|
||||
|
||||
@@ -205,6 +205,13 @@ async fn backfill(
|
||||
resp.more_remaining = more_pages || more_covers;
|
||||
}
|
||||
|
||||
// Audit is written after the action here *by necessity*, not oversight
|
||||
// (unlike reenqueue/analyze_page, which wrap their single DB mutation +
|
||||
// audit in one tx). The backfill is a budgeted scan that interleaves
|
||||
// filesystem `storage.size()` reads with per-batch DB writes across many
|
||||
// iterations; wrapping it in a transaction would hold one open across all
|
||||
// that I/O (a long-running tx). The batch size-writes are also idempotent
|
||||
// recomputations, so a lost audit row has negligible impact.
|
||||
repo::admin_audit::insert(
|
||||
&state.db,
|
||||
admin.0.id,
|
||||
|
||||
Reference in New Issue
Block a user