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:
+13
-3
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user