feat(recoverable job): add pause/resume capability
This commit is contained in:
@@ -37,5 +37,5 @@ pub use recoverable::{
|
||||
RecoverableJobHandler, RunOutcome, RunProgress, RunStatus, RunSummary, derive_progress,
|
||||
record_or_log, run_or_resume,
|
||||
};
|
||||
pub use registry::{JobEntry, JobRegistry, JobSummary, RegisterError};
|
||||
pub use registry::{JobEntry, JobRegistry, JobSummary, PausedRunBrief, RegisterError};
|
||||
pub use types::{ErrCause, JobOutcome, JobRunArgs};
|
||||
|
||||
@@ -320,6 +320,18 @@ pub trait JobStore: Send + Sync {
|
||||
/// stamped.
|
||||
async fn get_string_param(&self, key: &str) -> Result<Option<String>, DomainError>;
|
||||
|
||||
/// Current `stats.scanned_count` for this run. Used by handlers
|
||||
/// on a Resume path to reconstruct progress state that isn't
|
||||
/// persisted in `params` — e.g. `backend_migration` seeds its
|
||||
/// user-facing `MigrationProgress` counter with this so the
|
||||
/// admin banner shows continued progress across a restart
|
||||
/// instead of resetting to 0.
|
||||
///
|
||||
/// Returns `0` if the key is absent (fresh row) or not a
|
||||
/// number. Callers on a Fresh run can safely skip this — the
|
||||
/// answer is trivially 0 and the write path starts fresh.
|
||||
async fn scanned_count(&self) -> Result<u64, DomainError>;
|
||||
|
||||
/// Persist one finding to `jobs.run_findings` and bump
|
||||
/// `stats.finding_count` on the parent run. Consistency handlers
|
||||
/// call this in place of the transitional
|
||||
|
||||
@@ -226,6 +226,11 @@ impl JobRegistry {
|
||||
last_outcome,
|
||||
running: state.current_run_start.is_some(),
|
||||
recoverable: entry.handler.is_recoverable(),
|
||||
// Populated in `list_jobs` handler via a single
|
||||
// DB round-trip — kept out of the registry
|
||||
// snapshot to avoid pulling a DB dependency into
|
||||
// the in-memory scheduler state.
|
||||
paused_run: None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -293,6 +298,24 @@ pub enum RegisterError {
|
||||
/// `jobs.recoverable_runs`. Consumed by the admin UI to decide
|
||||
/// whether the row is expandable (drawer with run history +
|
||||
/// findings) and to gate the retention/purge action.
|
||||
/// Enough info about a paused recoverable run for the admin panel
|
||||
/// to render "Resume (scanned/total)" on the job row without opening
|
||||
/// the drawer. Populated by `list_jobs` in the admin handler from a
|
||||
/// single `SELECT job_name, id, stats->>'scanned_count',
|
||||
/// params->>'total_rows' FROM jobs.recoverable_runs WHERE status =
|
||||
/// 'Paused'` — indexed by the `one_active_run_per_job` partial UNIQUE.
|
||||
///
|
||||
/// `total` is `None` when the tenant doesn't seed a countable subject
|
||||
/// (`RecoverableJobHandler::count_total`); the UI then shows just
|
||||
/// "Resume" without progress.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PausedRunBrief {
|
||||
pub id: uuid::Uuid,
|
||||
pub scanned: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct JobSummary {
|
||||
pub name: String,
|
||||
@@ -306,6 +329,12 @@ pub struct JobSummary {
|
||||
pub last_outcome: Option<JobOutcome>,
|
||||
pub running: bool,
|
||||
pub recoverable: bool,
|
||||
/// Populated iff a `Paused` row exists in `jobs.recoverable_runs`
|
||||
/// for this job. Distinct from `running` — a paused run is
|
||||
/// resumable via the same trigger endpoint (`run_or_resume`
|
||||
/// picks Resume when the latest row is Paused).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub paused_run: Option<PausedRunBrief>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user