disc/patch: leave failed reads NonTrimmed mid-multipass, not Unreadable
User design call after watching Pass 2 mark ~20 KB as "Cosmetic" (permanently Unreadable) after just 10 retries within a single pass: "i think it's good or maybe until all passes are done. then it's gone." That contradicts what the multi-pass design promises a user. The project goal in CLAUDE.md is "recover 100% of readable data from any optical disc, automatically." Marking sectors Unreadable after a SINGLE pass's per-range retry budget gives up on sectors that subsequent passes might recover — drive reads are stochastic, the sector that fails 10 times in Pass 2 may succeed on attempt 1 in Pass 3 after temperature / bus state / prior-read patterns shift. The patch.rs doc comment already noted ~36% of patch-marked Unreadable sectors turned out to be readable in re-rip experiments. Three sites in `Disc::patch` were emitting `PatchItem::Unreadable` mid-pass: - backtrack hit damage (line ~2659) - all-retries-exhausted on a single LBA (line ~2846) - redundant second mark after the wedge-suspicion log (line ~2970) All three now emit `PatchItem::NonTrimmed` instead. Failed bytes stay "maybe" (NonTrimmed) so the next pass gets another shot. The per-range skip-limit (10) and per-pass wedge-threshold (50) still bound time-per-pass; they just no longer turn the bytes terminal. The `PatchItem::Unreadable` variant stays in the enum (with #[allow(dead_code)]) because the orchestrator-side end-of-recovery promotion will use it: autorip, after the final retry pass completes, scans the mapfile and promotes still-NonTrimmed → Unreadable. That promotion lands in a follow-up commit on the autorip side — separable from this libfreemkv change. Loss accounting unchanged: `bytes_pending + bytes_unreadable` is the "lost or pending" total that `abort_on_lost_secs` consults (disc/mod.rs:1327). Moving bytes from one bucket to the other mid-pass doesn't affect whether the rip would abort; it only affects display (UI shows "Maybe" vs "Cosmetic") and whether subsequent passes retry the bytes (the actual fix). Test update: `test_pass_progress_separates_unreadable_from_pending` was renamed to `test_pass2_leaves_failed_reads_as_pending_not_unreadable` and rewritten to assert the new invariant — Pass 2 leaves all failed bytes as bytes_pending (no mid-pass Unreadable promotion). Original assertions were checking the pre-design-call behavior. Precommit (cargo +1.86 fmt + clippy + test) green.
This commit is contained in:
+25
-6
@@ -70,14 +70,33 @@ pub(super) enum PatchItem {
|
||||
/// Producer exhausted retries on `[pos, pos+len)`. Consumer records
|
||||
/// the range as `Unreadable`. No file write — the existing zero-fill
|
||||
/// from sweep is preserved in place.
|
||||
///
|
||||
/// Currently unused by `Disc::patch` itself (2026-05-11 design call:
|
||||
/// patch never marks `Unreadable` mid-multipass; bytes stay
|
||||
/// `NonTrimmed` so future passes get another shot at them). Kept
|
||||
/// in the enum for the orchestrator-side end-of-recovery promotion
|
||||
/// (autorip, after the final retry pass completes, promotes
|
||||
/// still-NonTrimmed bytes to Unreadable). When that ships, this
|
||||
/// becomes the variant the orchestrator emits to the same
|
||||
/// PatchSink.
|
||||
#[allow(dead_code)]
|
||||
Unreadable { pos: u64, len: u64 },
|
||||
|
||||
/// Producer hit the per-range skip limit and is leaving the
|
||||
/// remaining bytes as `NonTrimmed` for a future pass. CRITICAL:
|
||||
/// this is not the same as `Unreadable` — sectors we never tried
|
||||
/// stay hopeful. (See the comment at the skip-limit branch in
|
||||
/// `Disc::patch`: ~36% of patch-marked Unreadable sectors are
|
||||
/// actually readable on a later pass.) No file write.
|
||||
/// Producer marks `[pos, pos+len)` as `NonTrimmed`. Used for BOTH
|
||||
/// the per-range skip-limit case (remaining bytes never tried) AND
|
||||
/// individual sector failures (tried-but-failed within a pass).
|
||||
/// Both stay "hopeful" — a later pass retries them.
|
||||
///
|
||||
/// CRITICAL: "NonTrimmed in pass N" does NOT mean "Unreadable
|
||||
/// forever." Drive reads are stochastic: the same sector that
|
||||
/// fails 10 times in Pass 2 may succeed on attempt 1 in Pass 3
|
||||
/// after temperature / bus state / prior-read patterns shift.
|
||||
/// Pre-2026-05-11 patch marked individual failures Unreadable,
|
||||
/// which gave up on sectors that subsequent passes could have
|
||||
/// recovered (historical: ~36% of patch-marked Unreadable
|
||||
/// sectors turned out to be readable in re-rip experiments).
|
||||
/// Promotion to true Unreadable is the orchestrator's job,
|
||||
/// applied once after all retry passes complete.
|
||||
NonTrimmed { pos: u64, len: u64 },
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user