disc/patch: fast_capture mode — breadth-first recovery (#50)
A PatchOptions.fast_capture pass reads each bad range ONCE at the full batch and leaves every FAILED block NonTrimmed for a later pass — no bisect, no per-sector grind, no retry. This lets a first retry pass grab the readable blocks (the sweep's good skip-ahead overshoot) of EVERY section quickly, before any single section's slow per-sector recovery — instead of grinding section 1 to exhaustion before even touching section 2. A later pass (fast_capture = false) does the granular bisect/retry on what's left. Load-bearing invariant (fixture test): NO data is dropped. A failed block becomes NonTrimmed (pending, retried by a granular pass), NEVER Unreadable. The test pins that the readable half of a range recovers, the bad half stays NonTrimmed (not Unreadable), and the bad block is marked in ONE batch read with zero bisection. Disc::copy's internal patch keeps fast_capture=false (single-call full recovery).
This commit is contained in:
@@ -2884,6 +2884,10 @@ impl Disc {
|
|||||||
progress: opts.progress,
|
progress: opts.progress,
|
||||||
halt: opts.halt.clone(),
|
halt: opts.halt.clone(),
|
||||||
key_fetch: opts.key_fetch.clone(),
|
key_fetch: opts.key_fetch.clone(),
|
||||||
|
// Disc::copy's internal patch grinds each range fully (it's a
|
||||||
|
// single-call recovery); the breadth-first fast-capture ordering is
|
||||||
|
// an autorip multi-pass concern.
|
||||||
|
fast_capture: false,
|
||||||
};
|
};
|
||||||
let pr = self.patch(reader, path, &patch_opts)?;
|
let pr = self.patch(reader, path, &patch_opts)?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -3799,6 +3803,18 @@ pub struct PatchOptions<'a> {
|
|||||||
/// On-decrypt-miss key fetch (see [`CopyOptions::key_fetch`]). Lets Pass N
|
/// On-decrypt-miss key fetch (see [`CopyOptions::key_fetch`]). Lets Pass N
|
||||||
/// recover an orphan CPS unit's key when re-reading its bad range.
|
/// recover an orphan CPS unit's key when re-reading its bad range.
|
||||||
pub key_fetch: Option<crate::sector::KeyFetch>,
|
pub key_fetch: Option<crate::sector::KeyFetch>,
|
||||||
|
/// Fast-capture pass: read each bad range ONCE at the full batch and leave
|
||||||
|
/// every failed block `NonTrimmed` for a later pass — WITHOUT bisecting,
|
||||||
|
/// re-reading, or grinding it here. This lets a first retry pass grab the
|
||||||
|
/// readable blocks (the sweep's good skip-ahead overshoot) of EVERY range
|
||||||
|
/// quickly, before any single range's slow per-sector recovery — so
|
||||||
|
/// recovered data surfaces across the whole disc first instead of grinding
|
||||||
|
/// section 1 to exhaustion before even touching section 2. A later pass
|
||||||
|
/// (`fast_capture = false`) does the granular bisect/retry on what's left.
|
||||||
|
/// No data is dropped: a failed block stays `NonTrimmed` until a granular
|
||||||
|
/// pass recovers it or finally gives up. A transport fault (bridge crash)
|
||||||
|
/// still aborts — it isn't a recoverable bad sector.
|
||||||
|
pub fast_capture: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result returned by [`Disc::patch`].
|
/// Result returned by [`Disc::patch`].
|
||||||
|
|||||||
@@ -1972,6 +1972,30 @@ impl<R: SectorSource + ?Sized> PatchCtx<'_, '_, R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
// Fast-capture pass: don't grind this block. Mark it
|
||||||
|
// NonTrimmed for a later granular pass and move on, so the
|
||||||
|
// readable blocks of EVERY range are captured before any
|
||||||
|
// single range's slow per-sector recovery. A transport fault
|
||||||
|
// (bridge crash) still falls through below — it isn't a
|
||||||
|
// recoverable bad sector. No data is dropped: the block stays
|
||||||
|
// NonTrimmed until a granular pass recovers or gives up on it.
|
||||||
|
if self.opts.fast_capture && !err.is_scsi_transport_failure() {
|
||||||
|
send_or_abort(
|
||||||
|
self.pipe,
|
||||||
|
PatchItem::NonTrimmed {
|
||||||
|
pos,
|
||||||
|
len: block_bytes,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
self.state.blocks_read_failed += 1;
|
||||||
|
if self.opts.reverse {
|
||||||
|
frame.block_end = frame.block_end.saturating_sub(block_bytes);
|
||||||
|
} else {
|
||||||
|
frame.block_end += block_bytes;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// First failure in this range: the fast-batched pass over
|
// First failure in this range: the fast-batched pass over
|
||||||
// the clean overshoot is done. A genuine transport fault
|
// the clean overshoot is done. A genuine transport fault
|
||||||
// (bridge crash) is NOT a recoverable bad sector — let
|
// (bridge crash) is NOT a recoverable bad sector — let
|
||||||
@@ -2481,6 +2505,7 @@ mod tests {
|
|||||||
halt: None,
|
halt: None,
|
||||||
|
|
||||||
key_fetch: None,
|
key_fetch: None,
|
||||||
|
fast_capture: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ fn patch_block_sectors_zero_does_not_busy_spin() {
|
|||||||
progress: None,
|
progress: None,
|
||||||
halt: Some(halt.clone()),
|
halt: Some(halt.clone()),
|
||||||
key_fetch: None,
|
key_fetch: None,
|
||||||
|
fast_capture: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let outcome = disc.patch(&mut reader, &iso_path, &opts);
|
let outcome = disc.patch(&mut reader, &iso_path, &opts);
|
||||||
|
|||||||
@@ -819,3 +819,105 @@ fn profile_08_batch_fail_singles_ok() {
|
|||||||
// If the unification ever proceeds, the next step is to add a clock
|
// If the unification ever proceeds, the next step is to add a clock
|
||||||
// injection point in `handle_read_failure` and extend this fixture
|
// injection point in `handle_read_failure` and extend this fixture
|
||||||
// with the wedge/NOT_READY profiles too.
|
// with the wedge/NOT_READY profiles too.
|
||||||
|
|
||||||
|
// ─────────────── Fast-capture (breadth-first) recovery — #50 ───────────────
|
||||||
|
//
|
||||||
|
// `fast_capture = true` reads each bad range ONCE at the batch size and leaves
|
||||||
|
// every FAILED block NonTrimmed for a later pass — no bisection, no per-sector
|
||||||
|
// grind. This is the breadth-first "fast-capture every section first, then
|
||||||
|
// escalate" ordering: a first retry pass grabs every range's readable blocks
|
||||||
|
// quickly instead of grinding section 1 to exhaustion before touching section 2.
|
||||||
|
//
|
||||||
|
// The load-bearing invariant: NO data is dropped. A failed block becomes
|
||||||
|
// NonTrimmed (pending, retried by a later granular pass), NEVER Unreadable.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fast_capture_keeps_readable_blocks_and_leaves_bad_nontrimmed_unbisected() {
|
||||||
|
let capacity_sectors: u32 = 256;
|
||||||
|
let (mut reader, trace) = ScriptedSectorReader::new(capacity_sectors);
|
||||||
|
// One bad sector at LBA 130 — inside the LOW 32-sector block of the range.
|
||||||
|
reader.always(
|
||||||
|
130,
|
||||||
|
ScriptStep::Err {
|
||||||
|
sense_key: 3,
|
||||||
|
asc: 0x11,
|
||||||
|
ascq: 0x05,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let total_bytes = capacity_sectors as u64 * SECTOR_SIZE as u64;
|
||||||
|
let disc = synthetic_disc(capacity_sectors);
|
||||||
|
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
let iso_path = tmp.path().to_path_buf();
|
||||||
|
drop(tmp);
|
||||||
|
// 64-sector NonTrimmed range [128,192); everything else already Finished.
|
||||||
|
let nontrimmed = [(128 * 2048, 64 * 2048)];
|
||||||
|
let finished = [
|
||||||
|
(0, 128 * 2048),
|
||||||
|
(192 * 2048, (capacity_sectors as u64 - 192) * 2048),
|
||||||
|
];
|
||||||
|
prep_iso_and_mapfile(&iso_path, total_bytes, &finished, &nontrimmed);
|
||||||
|
|
||||||
|
let opts = libfreemkv::disc::PatchOptions {
|
||||||
|
decrypt: false,
|
||||||
|
block_sectors: Some(32),
|
||||||
|
full_recovery: true,
|
||||||
|
reverse: true,
|
||||||
|
wedged_threshold: 50,
|
||||||
|
progress: None,
|
||||||
|
halt: None,
|
||||||
|
key_fetch: None,
|
||||||
|
fast_capture: true,
|
||||||
|
};
|
||||||
|
disc.patch(&mut reader, &iso_path, &opts)
|
||||||
|
.expect("fast-capture patch must not error");
|
||||||
|
|
||||||
|
let map_path = libfreemkv::disc::mapfile_path_for(&iso_path);
|
||||||
|
let stats = Mapfile::load(&map_path).unwrap().stats();
|
||||||
|
|
||||||
|
// The clean 32-block [160,192) recovered (+32 sectors over the 192 already
|
||||||
|
// Finished); the bad 32-block [128,160) is left NonTrimmed — NOT Unreadable.
|
||||||
|
// fast_capture never gives up; the next (granular) pass retries it.
|
||||||
|
// Conservation: the 64-sector range split into 32 good + 32 still-pending,
|
||||||
|
// nothing lost.
|
||||||
|
assert_eq!(
|
||||||
|
stats.bytes_unreadable, 0,
|
||||||
|
"fast capture must never mark Unreadable"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stats.bytes_pending,
|
||||||
|
32 * 2048,
|
||||||
|
"the bad block stays NonTrimmed for the next pass"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
stats.bytes_good,
|
||||||
|
224 * 2048,
|
||||||
|
"192 pre-Finished + 32 newly recovered"
|
||||||
|
);
|
||||||
|
|
||||||
|
// No bisection: the range [128,192) is read in exactly TWO 32-sector batch
|
||||||
|
// reads (clean half + bad half). Full mode would halve [128,160) into
|
||||||
|
// count=16,8,…,1 reads to isolate sector 130; fast capture marks the whole
|
||||||
|
// 32-block NonTrimmed in one read. (Reads outside the range — e.g. a lone
|
||||||
|
// count=1 probe at the capacity edge — are unrelated and ignored.)
|
||||||
|
let t = trace.lock().unwrap();
|
||||||
|
let range_reads: Vec<_> = t
|
||||||
|
.iter()
|
||||||
|
.filter(|&&(lba, _, _)| (128..192).contains(&lba))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
range_reads.len(),
|
||||||
|
2,
|
||||||
|
"range read in 2 batches (clean + bad), no bisection; trace={:?}",
|
||||||
|
*t
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
range_reads.iter().all(|&&(_, count, _)| count == 32),
|
||||||
|
"fast capture must not bisect — both range reads are the full batch; trace={:?}",
|
||||||
|
*t
|
||||||
|
);
|
||||||
|
drop(t);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&iso_path);
|
||||||
|
let _ = std::fs::remove_file(&map_path);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user