disc/patch: add scatter-recovery for stuck bad sectors
The 'reset, read good data, come back for one sector' technique. A genuinely-damaged sector makes the drive grind its full recovery timeout and still fail; re-reading consecutive bad LBAs at identical conditions re-fails AND is the rapid-failure cadence that drops the BU40N into a firmware fast-fail wedge (CLAUDE.md hard-rule #2). scatter_recover: on a stuck single sector, make up to SCATTER_MAX_ATTEMPTS fresh tries, each preceded by read_good_sectors() — a seek to a far known-good anchor + a sustained read that re-seats the head/servo and breaks the failure cadence. The fresh re-read uses the FAST timeout (recovery=false) so a recalibrated marginal sector reads quick and a truly-dead one fails fast instead of burning 60s per attempt. The seek + good read IS the settle (matches ddrescue/MakeMKV) — no idle sleep. Gated to genuine single-sector MEDIUM_ERROR (0x03): transport faults still abort the pass, NOT_READY keeps its own retry path, wedge-family senses are left for cooldown/eject. A scatter recovery is recorded exactly like a normal read success (write-then-mark-Finished); nothing marks a range good without a successful read + write. Logs anchor_ms/reread_ms per attempt for live tuning. 5 fixture tests (recover-marginal, give-up-dead, skip-non-medium, skip-batch, AACS widen); 2 AB read-count guards raised for scatter's bounded extra reads. Precommit green on Rust 1.86.
This commit is contained in:
@@ -397,10 +397,14 @@ fn profile_02_all_medium_error() {
|
||||
);
|
||||
assert!(!pr.halted, "02_all_medium halted");
|
||||
// Upper bound: every sector probed individually + a few batch-drop
|
||||
// and skip-escalation attempts. 16 sectors × ~3 visits ≈ 50.
|
||||
// and skip-escalation attempts, PLUS scatter-recovery on each hard
|
||||
// single sector (up to SCATTER_MAX_ATTEMPTS fresh tries, each a
|
||||
// recalibration read + a re-read = +6 reads/sector). Still strictly
|
||||
// bounded — the guard exists to catch an UNBOUNDED retry loop, which
|
||||
// would be in the hundreds.
|
||||
assert!(
|
||||
trace_len <= 80,
|
||||
"02_all_medium trace_len={trace_len} exceeds 80"
|
||||
trace_len <= 200,
|
||||
"02_all_medium trace_len={trace_len} exceeds 200"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -636,9 +640,13 @@ fn profile_06_deep_pit() {
|
||||
"06_deep_pit bytes_pending expected > 0"
|
||||
);
|
||||
assert!(!pr.halted, "06_deep_pit halted");
|
||||
// Bounded as in profile 02: the deep pit's hard single sectors each get
|
||||
// scatter-recovery (up to SCATTER_MAX_ATTEMPTS recalibrate + re-read
|
||||
// tries) on top of the baseline probe/skip walk. Still bounded — a
|
||||
// runaway loop would be in the hundreds.
|
||||
assert!(
|
||||
trace_len <= 120,
|
||||
"06_deep_pit trace_len={trace_len} exceeds 120"
|
||||
trace_len <= 180,
|
||||
"06_deep_pit trace_len={trace_len} exceeds 180"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user