disc: wedge AVOIDANCE on Pass 1 — inter-error pause + larger jumps
Complements the wedge-skip backstop (d7f1862) with proactive avoidance so we don't HIT the wedge in the first place. User's take after seeing the Dune Pt 2 rip wedge at 48%: 'we shouldn't be wedging.' Empirical observations from the 23:09:12-23:09:55 wedge timeline: 5 read errors over 43 s, ~8 s apart (drive's own ECC recovery takes 5-10 s per failure). Not 'hammering' in any usual sense, but cumulative firmware-state buildup over 5 in-cluster errors was enough to tip the BU40N into wedge mode at the 5th error. Damage cluster spanned ~140 MB (LBAs 19.898M-19.965M). Current damage-jump base of 256 sectors × batch=32 = 16 MB first jump, doubling to 32 MB, 64 MB... Each jump landed BACK INSIDE the 140 MB cluster, exposing the drive to MORE in-cluster errors. Two avoidance levers: 1. Inter-error pause on Pass 1 (PASS_1_FAIL_PAUSE_SECS = 5 s): pre-fix Pass 1 ran pause_secs=0 on all errors to 'zoom past' damage zones. Successful reads still zoom at zero pause — the pause applies only to FAILED reads, giving the drive's firmware cool-down between cluster exposures. Cost: ~5 s per scattered failure (~30-60 s total on a damage cluster); trivial vs. crashing the rip at 48%. 2. Larger damage-jump base (JUMP_BASE_SECTORS = 1024, up from 256): first jump at batch=32 now covers 64 MB instead of 16 MB, second jump 128 MB instead of 32 MB. Two jumps clear 192 MB — well past most single-cluster damage patterns. Smaller jumps were landing inside the cluster and adding to the wedge counter. Plus a halt-aware sleep helper (sleep_secs_or_halt) so the new inter-error pause doesn't degrade halt response time. Halt poll granularity 100 ms — halt fires within ~100 ms regardless of remaining pause time. Updated three sleep call sites in disc/mod.rs (SkipBlock pause, JumpAhead post-pause, Retry pause). The wedge-SKIP backstop (d7f1862) stays — combined with this avoidance work, the flow becomes: damage cluster encountered → pause 5 s, mark NonTrimmed → second failure → pause 5 s, mark NonTrimmed → ... threshold hit → damage-jump 64 MB (clears 95% of clusters) → if jump lands in another cluster: 128 MB next jump → only if drive STILL wedges after all this: wedge-skip kicks in (1 GB jump + 30 s cooldown × 16 budget) Tests: pass_1_pauses_briefly_on_skip_for_wedge_avoidance — locks the new 5 s pause behavior in place (replaces the old pause=0 test). integration test threshold bumped from 5 s to 60 s with comment explaining the new bound is 'not infinite' rather than 'milliseconds-fast'. All 433+ tests green on cargo +1.86 fmt + clippy + test. Precommit green.
This commit is contained in:
+31
-9
@@ -1634,9 +1634,7 @@ impl Disc {
|
||||
|
||||
match action {
|
||||
read_error::ReadAction::Retry { pause_secs } => {
|
||||
if pause_secs > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(pause_secs));
|
||||
}
|
||||
sleep_secs_or_halt(pause_secs, opts.halt.as_ref());
|
||||
}
|
||||
read_error::ReadAction::Bisect => {
|
||||
read_ctx.bisecting = true;
|
||||
@@ -1713,9 +1711,7 @@ impl Disc {
|
||||
break 'outer;
|
||||
}
|
||||
bytes_done = bytes_done.saturating_add(block_bytes);
|
||||
if pause_secs > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(pause_secs));
|
||||
}
|
||||
sleep_secs_or_halt(pause_secs, opts.halt.as_ref());
|
||||
pos += block_bytes;
|
||||
}
|
||||
read_error::ReadAction::JumpAhead {
|
||||
@@ -1770,9 +1766,7 @@ impl Disc {
|
||||
"damage-jump"
|
||||
);
|
||||
pos = jump_pos;
|
||||
if pause_secs > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(pause_secs));
|
||||
}
|
||||
sleep_secs_or_halt(pause_secs, opts.halt.as_ref());
|
||||
}
|
||||
read_error::ReadAction::AbortPass => {
|
||||
let (status, sense) = extract_scsi_context(&err);
|
||||
@@ -1974,6 +1968,34 @@ pub struct PatchOutcome {
|
||||
pub wedged_threshold: u64,
|
||||
}
|
||||
|
||||
/// Sleep `secs` seconds, but break early if `halt` flips to true.
|
||||
/// Used by Pass 1's wedge-avoidance inter-error pause so halt
|
||||
/// remains responsive regardless of how long the pause is.
|
||||
/// Polling granularity 100 ms — bounded latency on halt regardless
|
||||
/// of pause length.
|
||||
pub(crate) fn sleep_secs_or_halt(
|
||||
secs: u64,
|
||||
halt: Option<&std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||
) {
|
||||
if secs == 0 {
|
||||
return;
|
||||
}
|
||||
let Some(h) = halt else {
|
||||
std::thread::sleep(std::time::Duration::from_secs(secs));
|
||||
return;
|
||||
};
|
||||
let total = std::time::Duration::from_secs(secs);
|
||||
let slice = std::time::Duration::from_millis(100);
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < total {
|
||||
if h.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let remaining = total.saturating_sub(start.elapsed());
|
||||
std::thread::sleep(remaining.min(slice));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mapfile_path_for(iso_path: &std::path::Path) -> std::path::PathBuf {
|
||||
let mut s = iso_path.as_os_str().to_os_string();
|
||||
s.push(".mapfile");
|
||||
|
||||
Reference in New Issue
Block a user