v0.20.2: delete dead retry block + wedge-family cooldown in Disc::patch
WO-3c: Delete dead non-NOT_READY retry block (~100 LOC). The block declared retry_count = 0 inside the per-iteration Err arm, so the 'MAX_NON_NOT_READY_RETRIES=3' budget actually fired exactly once (1s pause + 1 retry) before falling through to NonTrimmed. The 'exponential backoff: 2s, 4s, 8s' comment was wrong by construction. Cross-pass NonTrimmed retry (each pass gives the same sectors another shot) already covers the recovery case, and gives the drive minutes between attempts instead of 1-8 seconds — empirically what stochastic recovery on the BU40N actually needs. WO-4 (targeted slice): Add wedge-family cooldown on HARDWARE_ERROR / ILLEGAL_REQUEST senses. These are what the BU40N's firmware fast-fail state returns; every subsequent read in that state comes back in <100ms. Pre-fix patch hammered the drive: mark NonTrimmed, sleep 1s, advance, hit next wedge, mark, sleep 1s — exactly the rapid-retry cadence the firmware is sensitive to. Now a wedge-family sense triggers WEDGE_FAMILY_COOLDOWN_SECS=30 cooldown (matches read_error.rs's ZONE_ENTRY_COOLDOWN_SECS), and WEDGE_ABORT_THRESHOLD=16 consecutive wedges aborts the pass for autorip eject+reload. Any non-wedge read clears the counter. Also drops the duplicate NonTrimmed dispatch (Mapfile::record is idempotent so it wasn't a correctness bug, but it doubled per-failure consumer work).
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "0.20.1"
|
version = "0.20.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
|
|||||||
+77
-119
@@ -449,6 +449,15 @@ impl Disc {
|
|||||||
// Reset to 0 at the start of every range; declared without init
|
// Reset to 0 at the start of every range; declared without init
|
||||||
// because the per-range reset (below) always runs before any read.
|
// because the per-range reset (below) always runs before any read.
|
||||||
let mut consecutive_failures: u64;
|
let mut consecutive_failures: u64;
|
||||||
|
// Wedge-family (HARDWARE_ERROR / ILLEGAL_REQUEST) counter — resets
|
||||||
|
// on any non-wedge read result, accumulates across ranges on
|
||||||
|
// consecutive wedge senses. After WEDGE_ABORT_THRESHOLD wedges
|
||||||
|
// with no recovery in between, the pass aborts so autorip can
|
||||||
|
// eject + reload the disc (the only thing that reliably clears
|
||||||
|
// a real firmware fast-fail wedge).
|
||||||
|
let mut wedge_count: u32 = 0;
|
||||||
|
const WEDGE_FAMILY_COOLDOWN_SECS: u64 = 30;
|
||||||
|
const WEDGE_ABORT_THRESHOLD: u32 = 16;
|
||||||
let mut unreadable_count: u64 = 0;
|
let mut unreadable_count: u64 = 0;
|
||||||
let mut bytes_good_last = bytes_good_before;
|
let mut bytes_good_last = bytes_good_before;
|
||||||
let mut stall_start = std::time::Instant::now();
|
let mut stall_start = std::time::Instant::now();
|
||||||
@@ -1041,105 +1050,20 @@ impl Disc {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-NOT_READY errors (MEDIUM ERROR, ABORTED COMMAND, etc.),
|
// (Removed in 0.20.2) The previous code retried
|
||||||
// try additional retries before marking Unreadable. This is especially
|
// non-NOT_READY errors on encrypted discs with an
|
||||||
// important for encrypted UHD discs where decryption failures can
|
// "exponential backoff: 2s, 4s, 8s" comment — but
|
||||||
// manifest as read errors that succeed on retry.
|
// `retry_count` was declared inside the per-iteration
|
||||||
let mut retry_count = 0;
|
// `Err` arm so it reset to 0 every iteration. The
|
||||||
const MAX_NON_NOT_READY_RETRIES: u64 = 3;
|
// "MAX_NON_NOT_READY_RETRIES=3" budget actually fired
|
||||||
let should_retry = opts.decrypt && retry_count < MAX_NON_NOT_READY_RETRIES;
|
// exactly once (1s pause + 1 retry), then fell through
|
||||||
|
// to the NonTrimmed dispatch below. The block was a
|
||||||
if should_retry {
|
// 100-line illusion. Cross-pass NonTrimmed retry
|
||||||
tracing::info!(
|
// (next pass gives the same sectors another shot)
|
||||||
target: "freemkv::disc",
|
// already covers the recovery case it was supposed
|
||||||
phase = "patch_non_not_ready_retry",
|
// to handle — and it gives the drive minutes between
|
||||||
lba,
|
// attempts instead of 1-8 seconds, which empirically
|
||||||
err_code = err.code(),
|
// matters for stochastic recovery on the BU40N.
|
||||||
retry = retry_count + 1,
|
|
||||||
max_retries = MAX_NON_NOT_READY_RETRIES,
|
|
||||||
"Non-NOT_READY error on encrypted disc; retrying"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Exponential backoff: 2s, 4s, 8s before final Unreadable mark
|
|
||||||
let pause_secs = (1u64 << retry_count).min(8);
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs(pause_secs));
|
|
||||||
retry_count += 1;
|
|
||||||
|
|
||||||
// Retry the read
|
|
||||||
match reader.read_sectors(lba, count, &mut buf[..bytes], recovery) {
|
|
||||||
Ok(_) => {
|
|
||||||
blocks_read_ok += 1;
|
|
||||||
consecutive_failures = 0;
|
|
||||||
consecutive_good_since_skip += 1;
|
|
||||||
if consecutive_good_since_skip >= PASSN_ESCALATION_RESET_GOOD {
|
|
||||||
consecutive_skips_without_recovery = 0;
|
|
||||||
}
|
|
||||||
damage_window.push(true);
|
|
||||||
if damage_window.len() > PASSN_DAMAGE_WINDOW {
|
|
||||||
damage_window.remove(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
target: "freemkv::disc",
|
|
||||||
phase = "patch_retry_success",
|
|
||||||
lba,
|
|
||||||
retry_count,
|
|
||||||
"Retry succeeded after non-NOT_READY error"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Plaintext via DecryptingSectorSource;
|
|
||||||
// same path the original read takes.
|
|
||||||
let write_start = std::time::Instant::now();
|
|
||||||
tracing::debug!(
|
|
||||||
target: "freemkv::disc",
|
|
||||||
phase = "patch_write_start",
|
|
||||||
pos,
|
|
||||||
bytes,
|
|
||||||
"Starting ISO write"
|
|
||||||
);
|
|
||||||
send_or_abort(
|
|
||||||
&pipe,
|
|
||||||
PatchItem::Recovered {
|
|
||||||
pos,
|
|
||||||
buf: buf[..bytes].to_vec(),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
let write_duration_ms = write_start.elapsed().as_millis();
|
|
||||||
tracing::info!(
|
|
||||||
target: "freemkv::disc",
|
|
||||||
phase = "patch_write_ok",
|
|
||||||
pos,
|
|
||||||
bytes,
|
|
||||||
write_duration_ms,
|
|
||||||
"ISO write succeeded"
|
|
||||||
);
|
|
||||||
tracing::info!(
|
|
||||||
target: "freemkv::disc",
|
|
||||||
phase = "patch_mapfile_record_ok",
|
|
||||||
pos,
|
|
||||||
block_bytes,
|
|
||||||
"Mapfile record dispatched"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Stall guard after successful retry
|
|
||||||
let bytes_good_now = read_shared(&shared).0.bytes_good;
|
|
||||||
if bytes_good_now > bytes_good_last {
|
|
||||||
stall_start = std::time::Instant::now();
|
|
||||||
bytes_good_last = bytes_good_now;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
tracing::warn!(
|
|
||||||
target: "freemkv::disc",
|
|
||||||
phase = "patch_retry_failed",
|
|
||||||
lba,
|
|
||||||
retry_count,
|
|
||||||
"Retry failed after non-NOT_READY error"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All retries exhausted IN THIS PASS — leave NonTrimmed
|
// All retries exhausted IN THIS PASS — leave NonTrimmed
|
||||||
// so a subsequent pass gets another shot. Bytes stay
|
// so a subsequent pass gets another shot. Bytes stay
|
||||||
@@ -1282,27 +1206,56 @@ impl Disc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pair with the earlier NonTrimmed dispatch — same
|
// (Removed in 0.20.2) Duplicate NonTrimmed dispatch.
|
||||||
// bytes, same state. Pre-2026-05-11 this was a
|
// The earlier `send_or_abort(PatchItem::NonTrimmed)`
|
||||||
// second Unreadable mark; now it's NonTrimmed for
|
// already recorded the range. `Mapfile::record` is
|
||||||
// the same reason: cross-pass retry survival.
|
// idempotent so it wasn't a correctness bug, but
|
||||||
send_or_abort(
|
// it doubled the consumer's per-failure work.
|
||||||
&pipe,
|
|
||||||
PatchItem::NonTrimmed {
|
|
||||||
pos,
|
|
||||||
len: block_bytes,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
tracing::info!(
|
|
||||||
target: "freemkv::disc",
|
|
||||||
phase = "patch_mapfile_record_nontrimmed",
|
|
||||||
pos,
|
|
||||||
block_bytes,
|
|
||||||
consecutive_failures,
|
|
||||||
"Mapfile record dispatched as NonTrimmed (retry next pass)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let pause_secs = if err.is_bridge_degradation() {
|
// Wedge-family detection: HARDWARE_ERROR / ILLEGAL_REQUEST
|
||||||
|
// are the senses the BU40N's firmware fast-fail mode
|
||||||
|
// returns. When the drive is wedged, every subsequent
|
||||||
|
// read returns these in <100ms — exactly the rapid-retry
|
||||||
|
// cadence that bricks the drive further. Long cooldown
|
||||||
|
// (30s, matching read_error::ZONE_ENTRY_COOLDOWN_SECS)
|
||||||
|
// gives the firmware breathing room to clear the fast-
|
||||||
|
// fail state. After WEDGE_ABORT_THRESHOLD consecutive
|
||||||
|
// wedge senses with no recovery, bail to autorip so it
|
||||||
|
// can eject + reload (the only thing that reliably
|
||||||
|
// clears a real wedge).
|
||||||
|
let is_wedge_family = err
|
||||||
|
.scsi_sense()
|
||||||
|
.map(|s| {
|
||||||
|
s.sense_key == crate::scsi::SENSE_KEY_HARDWARE_ERROR
|
||||||
|
|| s.sense_key == crate::scsi::SENSE_KEY_ILLEGAL_REQUEST
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let pause_secs = if is_wedge_family {
|
||||||
|
wedge_count += 1;
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "patch_wedge_family",
|
||||||
|
lba,
|
||||||
|
wedge_count,
|
||||||
|
wedge_abort_threshold = WEDGE_ABORT_THRESHOLD,
|
||||||
|
sense_key = err.scsi_sense().map(|s| s.sense_key as u32).unwrap_or(0),
|
||||||
|
"HARDWARE_ERROR / ILLEGAL_REQUEST sense — wedge family, applying long cooldown"
|
||||||
|
);
|
||||||
|
if wedge_count >= WEDGE_ABORT_THRESHOLD {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "patch_wedge_abort",
|
||||||
|
wedge_count,
|
||||||
|
WEDGE_ABORT_THRESHOLD,
|
||||||
|
"Drive appears wedged ({} consecutive wedge-family senses); aborting pass for autorip eject+reload",
|
||||||
|
wedge_count
|
||||||
|
);
|
||||||
|
wedged_exit = true;
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
WEDGE_FAMILY_COOLDOWN_SECS
|
||||||
|
} else if err.is_bridge_degradation() {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "patch_bridge_degradation",
|
phase = "patch_bridge_degradation",
|
||||||
@@ -1318,6 +1271,11 @@ impl Disc {
|
|||||||
POST_FAILURE_PAUSE_SECS
|
POST_FAILURE_PAUSE_SECS
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Any non-wedge-family read clears the wedge counter.
|
||||||
|
if !is_wedge_family {
|
||||||
|
wedge_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "patch_post_failure_pause",
|
phase = "patch_post_failure_pause",
|
||||||
|
|||||||
Reference in New Issue
Block a user