diff --git a/src/infrastructure/services/blobs_consistency_service.rs b/src/infrastructure/services/blobs_consistency_service.rs index 9f379750..068bbf21 100644 --- a/src/infrastructure/services/blobs_consistency_service.rs +++ b/src/infrastructure/services/blobs_consistency_service.rs @@ -251,6 +251,12 @@ impl RecoverableJobHandler for BlobsConsistencyCheck { ); } + // Snapshot "is this a Fresh run?" BEFORE the resume_cursor + // match consumes it — otherwise the `is_none()` check later + // borrows a partially-moved value. Fresh = no cursor bytes + // at all; Resumed = cursor bytes present (possibly empty). + let is_fresh = resume_cursor.is_none(); + // Cursor = the last-visited `hash` string, UTF-8-encoded. On // resume, we walk `WHERE hash > $cursor` in ASC order. First // batch: NULL cursor → start from the smallest hash. @@ -272,10 +278,49 @@ impl RecoverableJobHandler for BlobsConsistencyCheck { // `record_finding` on each emission). let mut finding_count = 0u64; - // Deep mode = re-hash bytes for bit-rot detection. Logged - // once at run start so operators tailing tracing know why the - // scan is taking hours. - if args.deep { + // Deep mode is a per-run flag with two consumers: + // 1. This handler — decides whether to re-hash bytes. + // 2. The admin panel — needs to display whether the run + // was deep so operators know what the scan actually + // verified. + // + // On a Fresh run we take it from `deep` (the trigger + // endpoint stamps `?deep=true` onto the args) and stash it + // in `params.deep` so: + // * Resume picks up the same mode (would previously become + // non-deep on Resume — a Paused deep scan silently lost + // its `deep` intent). + // * The admin panel run-detail view can render + // `params.deep = "true"` alongside `target_name`, + // `progress_kind`, etc. + // + // Persist BEFORE the walk so a mid-fresh-batch crash still + // leaves a Paused row with the right mode marker. + let deep = if is_fresh { + let deep = args.deep; + let v = if deep { "true" } else { "false" }; + if let Err(e) = store.set_string_param("deep", v).await { + return RunOutcome::Failed { + message: format!("failed to persist deep flag to params: {e}"), + }; + } + deep + } else { + // Resumed run — read the persisted flag. Default to + // false (fast mode) if the row is a pre-K3.5 Paused + // scan without the param stashed. + match store.get_string_param("deep").await { + Ok(Some(v)) => v == "true", + Ok(None) => false, + Err(e) => { + return RunOutcome::Failed { + message: format!("read `deep` from params: {e}"), + }; + } + } + }; + + if deep { tracing::info!( target: "oxicloud::consistency", event = "blobs_consistency.deep_mode_active", @@ -377,7 +422,7 @@ impl RecoverableJobHandler for BlobsConsistencyCheck { event = "blobs_consistency.completed", run_id = %store.run_id(), finding_count = finding_count, - deep = args.deep, + deep = deep, "blobs_consistency completed with {} finding(s)", finding_count ); @@ -472,7 +517,7 @@ impl RecoverableJobHandler for BlobsConsistencyCheck { // `expected_hash` was NOT reused as a name to // avoid mistaking it for "the hash we expect to // see on disk (i.e. what will fix this)". - if args.deep { + if deep { match recompute_hash(backend.as_ref(), &row.hash).await { Ok(computed_hash) if computed_hash == row.hash => {} Ok(computed_hash) => { @@ -524,7 +569,7 @@ impl RecoverableJobHandler for BlobsConsistencyCheck { event = "blobs_consistency.completed", run_id = %store.run_id(), finding_count = finding_count, - deep = args.deep, + deep = deep, "blobs_consistency completed with {} finding(s)", finding_count );