Pass-N: detect drive fast-fail wedge and abort instead of grinding

The 2026-07-01 overnight rip ground a wedged BU40N for 28 minutes at
0 B/s. Root cause: the handler chain's read_span split errors only into
Transport (bus gone) vs Bad (everything else), so the drive's fast-fail
wedge sense (ILLEGAL REQUEST 0x05/0x24 — it rejects every CDB in <100ms
without attempting recovery) was treated as an ordinary bad sector. The
chain hopped to the next section forever, and the existing wedge detector
in read_error.rs never saw it because the chain reads through
recovery_read directly.

Add a pass-level wedge streak: read_span counts consecutive wedge-family
(Hardware/IllegalRequest) senses; at WEDGE_ABORT_STREAK (16) it escalates
the read to Transport, which every handler already propagates as
TransportFault — aborting the whole pass and setting wedged_exit so the
caller spin-cycles. The streak is carried across sections via PatchCtx
(seeded into and read back from each per-section HandlerCtx) so a wedge is
caught even when every bad sub-range is smaller than the streak. Any Good
or non-wedge (medium-error) read resets it, so scattered bad sectors on
real media never trip it. New fixture test: a wholly-wedged 1000-sector
section aborts in <100 reads, not 1000.
This commit is contained in:
Matthew Jackson
2026-07-01 08:02:52 -07:00
parent 8f6a92ccd4
commit fe14a2d5e5
2 changed files with 147 additions and 5 deletions
+13 -3
View File
@@ -751,6 +751,10 @@ struct PatchCtx<'a, 'o> {
/// coordinator runs the winners first and lets duds fall back. Reset per
/// pass (ephemeral, no persistence).
scoreboard: HandlerScoreboard,
/// Consecutive wedge-family senses across the WHOLE pass. Seeded into each
/// per-section `HandlerCtx` and read back after, so a drive fast-fail wedge is
/// detected even when every bad sub-range is smaller than the abort streak.
wedge_streak: u32,
}
impl PatchCtx<'_, '_> {
@@ -883,7 +887,7 @@ impl PatchCtx<'_, '_> {
};
let bad_before = bad.total_len();
let outcome = {
let (outcome, wedge_after) = {
// Progress heartbeat: a throttled closure that pushes a fresh
// snapshot to the reporter as recovery happens (called from every
// read via `HandlerCtx::progress`), so the bar and speed move DURING
@@ -913,11 +917,16 @@ impl PatchCtx<'_, '_> {
decrypt_is_aacs: self.decrypt_is_aacs,
tick: Some(&mut tick),
unproductive: 0,
// Carry the pass-level wedge streak in so a fast-fail wedge is
// caught across many small sections, not reset each one.
wedge_streak: self.wedge_streak,
};
run_handlers(&mut ctx, &mut handlers, bad, &mut self.scoreboard, |_bad| {
let o = run_handlers(&mut ctx, &mut handlers, bad, &mut self.scoreboard, |_bad| {
now_ptr() + std::time::Duration::from_secs(PER_HANDLER_BUDGET_SECS)
})
});
(o, ctx.wedge_streak)
};
self.wedge_streak = wedge_after;
tracing::info!(
target: "freemkv::disc",
@@ -1235,6 +1244,7 @@ impl Disc {
decrypt_is_aacs,
state: PatchLoopState::new(bytes_good_before, total_bytes, initial_batch, work_total),
scoreboard: HandlerScoreboard::default(),
wedge_streak: 0,
};
ctx.run(&bad_ranges)?;
ctx.scoreboard.log();