recovery: parameterize the read primitive with ReadParams (speed/FUA/timeout)

Add ReadParams { speed: SpeedPref, fua: bool, timeout: TimeoutPref } and
thread it through read_span so every wedge-safe handler read can request a
spindle speed (SET CD SPEED issued only on change, restored to max when the
handler exits), set the READ(10) FUA bit, and pick the 10s vs 60s timeout.

- SectorSource gains read_sectors_fua (default ignores fua); Drive sets the
  CDB bit, DecryptingSectorSource threads fua to its inner read.
- recovery_read gains a fua param.
- Linear becomes { direction, params }; Bisect/Jump take params. Existing
  tier-0/1 instances keep identical behavior (max speed, no FUA, fast/deep).
- Scoreboard keys on the full-config String name (linear:fwd:max:fast, ...).
- FakeDisc observes speed + FUA + approach so specialist techniques are
  provably exercised in later commits.

cargo test -p libfreemkv green (2193 passed).
This commit is contained in:
Matthew Jackson
2026-07-01 13:50:35 -07:00
parent e94319c099
commit 0c8153304e
5 changed files with 545 additions and 133 deletions
+71 -41
View File
@@ -59,8 +59,8 @@ use crate::io::pipeline::{Flow, Sink};
use super::mapfile::{self, MapStats, Mapfile, SectorStatus}; use super::mapfile::{self, MapStats, Mapfile, SectorStatus};
use super::section_recover::{ use super::section_recover::{
Bisect, HandlerCtx, HandlerOutcome, HandlerScoreboard, Jump, Linear, RecoverySink, Bisect, Direction, HandlerCtx, HandlerOutcome, HandlerScoreboard, Jump, Linear, ReadParams,
SectionHandler, run_handlers, RecoverySink, SectionHandler, run_handlers,
}; };
/// Wall-clock budget one recovery handler gets on a section before the chain /// Wall-clock budget one recovery handler gets on a section before the chain
@@ -426,8 +426,11 @@ pub(super) fn compute_initial_state(
/// 0, so the widened start is always unit-aligned. All recovery /// 0, so the widened start is always unit-aligned. All recovery
/// accounting upstream (pos, block_bytes, dispatched lba/count) is /// accounting upstream (pos, block_bytes, dispatched lba/count) is
/// unchanged — only the physical read widens, so the cursor cannot /// unchanged — only the physical read widens, so the cursor cannot
/// desync. `recovery` selects the SCSI timeout (true = 60 s deep /// desync. `recovery` selects the SCSI timeout (true = 60 s deep recovery,
/// recovery, false = the fast path). /// false = the fast path); `fua` forces the drive to bypass its readahead cache
/// and re-fetch
/// the medium (a Pass-N marginal-sector lever — see
/// [`crate::sector::SectorSource::read_sectors_fua`]).
pub(super) fn recovery_read<R: SectorSource + ?Sized>( pub(super) fn recovery_read<R: SectorSource + ?Sized>(
reader: &mut R, reader: &mut R,
decrypt_is_aacs: bool, decrypt_is_aacs: bool,
@@ -435,6 +438,7 @@ pub(super) fn recovery_read<R: SectorSource + ?Sized>(
count: u16, count: u16,
buf: &mut [u8], buf: &mut [u8],
recovery: bool, recovery: bool,
fua: bool,
) -> Result<usize> { ) -> Result<usize> {
let bytes = count as usize * 2048; let bytes = count as usize * 2048;
if decrypt_is_aacs && (lba % 3 != 0 || count % 3 != 0) { if decrypt_is_aacs && (lba % 3 != 0 || count % 3 != 0) {
@@ -444,11 +448,11 @@ pub(super) fn recovery_read<R: SectorSource + ?Sized>(
let span = head + count as usize; let span = head + count as usize;
let aligned_count = span + ((U as usize - span % U as usize) % U as usize); let aligned_count = span + ((U as usize - span % U as usize) % U as usize);
let mut scratch = vec![0u8; aligned_count * 2048]; let mut scratch = vec![0u8; aligned_count * 2048];
reader.read_sectors(aligned_lba, aligned_count as u16, &mut scratch, recovery)?; reader.read_sectors_fua(aligned_lba, aligned_count as u16, &mut scratch, recovery, fua)?;
buf[..bytes].copy_from_slice(&scratch[head * 2048..head * 2048 + bytes]); buf[..bytes].copy_from_slice(&scratch[head * 2048..head * 2048 + bytes]);
Ok(bytes) Ok(bytes)
} else { } else {
reader.read_sectors(lba, count, &mut buf[..bytes], recovery) reader.read_sectors_fua(lba, count, &mut buf[..bytes], recovery, fua)
} }
} }
@@ -740,6 +744,57 @@ struct PatchCtx<'a, 'o> {
wedge_streak: u32, wedge_streak: u32,
} }
/// Build the handler chain for one breadth-first tier. Each config is named by
/// its FULL parameterisation (`build_tier_handlers` picks the roster; the
/// scorecard re-orders WITHIN a tier per rip). The engine hardcodes no
/// conclusion: every technique is always present at its tier, and a technique
/// that doesn't fit this disc self-deprioritises (scores low, yields after 4
/// unproductive reads) rather than being removed.
///
/// - **Tier 0 — fast scouts** (`fast`: max speed, 10 s, cache on): grab the
/// readable bulk across every range.
/// - **Tier 1 — slow-deep** (`deep`: max speed, 60 s ECC budget): deep-recover
/// the easy residual.
/// - **Tier 2 — marginal specialists**: the physical-failure-mode matrix
/// (SlowSpin / FuaRetry / SlowFua / CachePrime / Oscillate / SpeedSweep), run
/// ONLY on what tiers 0-1 leave.
fn build_tier_handlers(tier: usize) -> Vec<Box<dyn SectionHandler>> {
match tier {
// Tier 0 — fast scouts. Bisect leads by default (probing a range's
// MIDDLE finds a readable island in one read); Jump blows through large
// dead runs; the fast linear sweeps mop up. The scorecard re-orders.
0 => vec![
Box::new(Bisect {
params: ReadParams::fast(),
}),
Box::new(Jump {
params: ReadParams::fast(),
}),
Box::new(Linear {
direction: Direction::Reverse,
params: ReadParams::fast(),
}),
Box::new(Linear {
direction: Direction::Forward,
params: ReadParams::fast(),
}),
],
// Tier 1 — slow deep recovery on the small residue tier 0 leaves.
1 => vec![
Box::new(Linear {
direction: Direction::Reverse,
params: ReadParams::deep(),
}),
Box::new(Linear {
direction: Direction::Forward,
params: ReadParams::deep(),
}),
],
// Tier 2 — marginal specialists (wired in when PATCH_TIERS reaches 3).
_ => Vec::new(),
}
}
impl PatchCtx<'_, '_> { impl PatchCtx<'_, '_> {
/// Orchestrator (one pass): walk the ordered bad ranges. Apply the /// Orchestrator (one pass): walk the ordered bad ranges. Apply the
/// inter-range cooldown only after a range that grinded, then recover /// inter-range cooldown only after a range that grinded, then recover
@@ -822,42 +877,15 @@ impl PatchCtx<'_, '_> {
"entering patch range" "entering patch range"
); );
// Enter at max read speed; a handler drops to the deep-recovery read // Enter at max read speed. A handler picks its own speed / FUA / timeout
// itself via its `fast` flag. // via its `ReadParams`; `read_span` restores max after each handler, so
// every tier starts from the streaming default.
self.reader.set_speed(0xFFFF); self.reader.set_speed(0xFFFF);
// Tier 0 = FAST scouts, ordered best-first by the rip scorecard (see // The tier roster (see `build_tier_handlers`): tier 0 fast scouts, tier 1
// run_handlers). Bisect leads by default because probing the MIDDLE of a // slow-deep, tier 2 marginal specialists. `run_handlers` orders WITHIN a
// range finds a readable island in one read, where Jump has to grind the // tier best-first by the rip scorecard, which re-learns per disc.
// dead front to reach it; Jump then blows through large dead runs, and let mut handlers: Vec<Box<dyn SectionHandler>> = build_tier_handlers(tier);
// the fast linear sweeps mop up. The scorecard re-orders these as it
// learns which one is actually winning on THIS disc. Tier 1 = slow deep
// recovery on the small residue tier 0 leaves.
let mut handlers: Vec<Box<dyn SectionHandler>> = if tier == 0 {
vec![
Box::new(Bisect),
Box::new(Jump),
Box::new(Linear {
reverse: true,
fast: true,
}),
Box::new(Linear {
reverse: false,
fast: true,
}),
]
} else {
vec![
Box::new(Linear {
reverse: true,
fast: false,
}),
Box::new(Linear {
reverse: false,
fast: false,
}),
]
};
// Clock seam: handlers read wall time through this so tests can wind a // Clock seam: handlers read wall time through this so tests can wind a
// fake clock (the same seam the pass uses for its own timing). // fake clock (the same seam the pass uses for its own timing).
@@ -903,6 +931,8 @@ impl PatchCtx<'_, '_> {
// Carry the pass-level wedge streak in so a fast-fail wedge is // Carry the pass-level wedge streak in so a fast-fail wedge is
// caught across many small sections, not reset each one. // caught across many small sections, not reset each one.
wedge_streak: self.wedge_streak, wedge_streak: self.wedge_streak,
// Drive was just reset to max above; read_span tracks changes.
cur_speed: 0xFFFF,
}; };
let o = 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) now_ptr() + std::time::Duration::from_secs(PER_HANDLER_BUDGET_SECS)
@@ -1399,7 +1429,7 @@ mod tests {
}; };
let mut buf = vec![0u8; 2048]; let mut buf = vec![0u8; 2048];
// Request lba=4 (4 % 3 == 1, mid-unit), count=1. // Request lba=4 (4 % 3 == 1, mid-unit), count=1.
let n = recovery_read(&mut rr, true, 4, 1, &mut buf, true).unwrap(); let n = recovery_read(&mut rr, true, 4, 1, &mut buf, true, false).unwrap();
assert_eq!(n, 2048); assert_eq!(n, 2048);
assert_eq!(rr.saw_lba, 3, "widened down to the unit-aligned start"); assert_eq!(rr.saw_lba, 3, "widened down to the unit-aligned start");
assert_eq!(rr.saw_count, 3, "widened to a whole 3-sector unit"); assert_eq!(rr.saw_count, 3, "widened to a whole 3-sector unit");
+346 -78
View File
@@ -86,6 +86,102 @@ const WEDGE_ABORT_STREAK: u32 = 16;
/// a true fast-fail. /// a true fast-fail.
const WEDGE_FASTFAIL_MS: u64 = 500; const WEDGE_FASTFAIL_MS: u64 = 500;
/// Max read speed sentinel for `SET CD SPEED` (0xFFFF = "as fast as the drive
/// will go"). The default for every read; a handler that wants to slow the
/// spindle passes [`SpeedPref::Min`] and [`read_span`] restores this on exit.
const SPEED_MAX_KBS: u16 = 0xFFFF;
/// Min read speed (~DVD 1×; the drive clamps up to its own supported minimum).
/// Slower rotation gives the servo more dwell and the ECC engine more
/// integration time per sector — the SlowSpin / SpeedSweep lever. The exact
/// value only has to be well below max; the drive rounds it to a supported step.
const SPEED_MIN_KBS: u16 = 1385;
/// Which spindle speed a read requests. `Max` is the streaming default; `Min`
/// slows the spindle for marginal-sector recovery (more servo dwell + ECC
/// integration). `Mid` is reserved for a future resonance step (SpeedSweep).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SpeedPref {
Max,
Min,
}
impl SpeedPref {
/// The `SET CD SPEED` value (KB/s) this preference maps to.
fn kbs(self) -> u16 {
match self {
SpeedPref::Max => SPEED_MAX_KBS,
SpeedPref::Min => SPEED_MIN_KBS,
}
}
}
/// Which SCSI read timeout a read requests. `Fast` is the 10 s single-attempt
/// budget (scouting); `Deep` is the 60 s ECC-recovery budget (deep recovery).
/// Maps onto `recovery_read`'s `recovery` bool.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum TimeoutPref {
Fast,
Deep,
}
impl TimeoutPref {
/// The `recovery` bool (true = 60 s deep) this timeout maps to.
fn recovery(self) -> bool {
matches!(self, TimeoutPref::Deep)
}
}
/// The per-read knobs a handler hands to [`read_span`]. A handler is a point in
/// the (direction × speed × cache × timeout) space; `ReadParams` carries the
/// speed / cache(FUA) / timeout axes (direction is the handler's own walk), so
/// the SAME read primitive serves every handler — a new technique is a new
/// *parameterisation*, never a bypass of the wedge-safe read path.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct ReadParams {
pub speed: SpeedPref,
pub fua: bool,
pub timeout: TimeoutPref,
}
impl ReadParams {
/// Tier-0 scout read: max speed, cache on, 10 s single-attempt.
pub(super) fn fast() -> Self {
Self {
speed: SpeedPref::Max,
fua: false,
timeout: TimeoutPref::Fast,
}
}
/// Tier-1 deep read: max speed, cache on, 60 s ECC-recovery budget.
pub(super) fn deep() -> Self {
Self {
speed: SpeedPref::Max,
fua: false,
timeout: TimeoutPref::Deep,
}
}
/// Scorecard tag for the speed / cache / timeout axes, e.g. `min:fua:deep`.
/// The handler prepends its own name + direction (`linear:fwd:` + tag).
fn tag(&self) -> String {
let speed = match self.speed {
SpeedPref::Max => "max",
SpeedPref::Min => "min",
};
let timeout = match self.timeout {
TimeoutPref::Fast => "fast",
TimeoutPref::Deep => "deep",
};
if self.fua {
format!("{speed}:fua:{timeout}")
} else {
format!("{speed}:{timeout}")
}
}
}
/// Where a handler left the section after its bounded attempt. /// Where a handler left the section after its bounded attempt.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum HandlerOutcome { pub(super) enum HandlerOutcome {
@@ -139,6 +235,13 @@ pub(super) struct HandlerCtx<'a> {
/// escalates to `Transport`. Seeded from and read back into the pass-level /// escalates to `Transport`. Seeded from and read back into the pass-level
/// counter so the streak spans sections; a Good or non-wedge read resets it. /// counter so the streak spans sections; a Good or non-wedge read resets it.
pub wedge_streak: u32, pub wedge_streak: u32,
/// The spindle speed (`SET CD SPEED` KB/s) currently programmed into the
/// drive. [`read_span`] issues `SET CD SPEED` only when a read's requested
/// speed DIFFERS from this (a `SET CD SPEED` per read would thrash the
/// spindle), and [`run_handlers`] restores [`SPEED_MAX_KBS`] after each
/// handler. Seeded to max — the caller resets the drive to max before the
/// chain runs.
pub cur_speed: u16,
} }
impl HandlerCtx<'_> { impl HandlerCtx<'_> {
@@ -196,7 +299,7 @@ fn read_span(
buf: &mut [u8], buf: &mut [u8],
pos: u64, pos: u64,
count: u16, count: u16,
recovery: bool, params: ReadParams,
) -> ReadHit { ) -> ReadHit {
let lba = (pos / SECTOR) as u32; let lba = (pos / SECTOR) as u32;
let bytes = count as usize * SECTOR as usize; let bytes = count as usize * SECTOR as usize;
@@ -208,8 +311,24 @@ fn read_span(
count >= 1 && pos % SECTOR == 0, count >= 1 && pos % SECTOR == 0,
"read_span requires a sector-aligned, >=1-sector span (pos={pos}, count={count})" "read_span requires a sector-aligned, >=1-sector span (pos={pos}, count={count})"
); );
// Program the spindle speed ONLY when it changes — a `SET CD SPEED` per read
// would thrash the drive. `run_handlers` restores max after the handler.
let want_speed = params.speed.kbs();
if want_speed != ctx.cur_speed {
ctx.reader.set_speed(want_speed);
ctx.cur_speed = want_speed;
}
let recovery = params.timeout.recovery();
let read_started = (ctx.now)(); let read_started = (ctx.now)();
let hit = match recovery_read(ctx.reader, ctx.decrypt_is_aacs, lba, count, buf, recovery) { let hit = match recovery_read(
ctx.reader,
ctx.decrypt_is_aacs,
lba,
count,
buf,
recovery,
params.fua,
) {
Ok(_) => { Ok(_) => {
ctx.sink.recovered(pos, &buf[..bytes]); ctx.sink.recovered(pos, &buf[..bytes]);
ReadHit::Good ReadHit::Good
@@ -275,7 +394,11 @@ fn read_span(
/// bad read leave it in `bad` and advance (skip-and-move-on); on a transport /// bad read leave it in `bad` and advance (skip-and-move-on); on a transport
/// fault return [`HandlerOutcome::TransportFault`] immediately. /// fault return [`HandlerOutcome::TransportFault`] immediately.
pub(super) trait SectionHandler { pub(super) trait SectionHandler {
fn name(&self) -> &'static str; /// Scorecard identity — the FULL config (technique + direction + speed +
/// cache + timeout), e.g. `linear:fwd:min:fua:deep`. The scoreboard keys on
/// this, so two instances of the same handler at different [`ReadParams`]
/// score independently and can flip past each other.
fn name(&self) -> String;
fn recover( fn recover(
&mut self, &mut self,
ctx: &mut HandlerCtx, ctx: &mut HandlerCtx,
@@ -284,25 +407,43 @@ pub(super) trait SectionHandler {
) -> HandlerOutcome; ) -> HandlerOutcome;
} }
/// Linear sweep of each bad sub-range. `reverse` walks end→start (the disc /// Which end a [`Linear`] sweep walks from.
/// sweep overshoots forward, so a NonTrimmed range's good data sits at its tail #[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// — reverse hits it first); `!reverse` walks start→end (the front the reverse pub(super) enum Direction {
/// pass kept dying on). `fast` selects the single-attempt read (`recovery = /// start→end (the front the reverse pass kept dying on).
/// false`) over the 60 s deep-recovery read. The two bools give backwards / Forward,
/// forwards / fast / slow from one handler. /// end→start (the disc sweep overshoots forward, so a NonTrimmed range's
/// good data sits at its tail — reverse hits it first).
Reverse,
}
impl Direction {
fn is_reverse(self) -> bool {
matches!(self, Direction::Reverse)
}
fn tag(self) -> &'static str {
match self {
Direction::Forward => "fwd",
Direction::Reverse => "rev",
}
}
}
/// Linear batch sweep of each bad sub-range, in `direction`, at `params`. The
/// direction × the [`ReadParams`] axes (speed / FUA / timeout) give every
/// backwards/forwards × fast/slow × max/min × cache/FUA combination from one
/// handler — the tier-0 fast scouts, the tier-1 deep sweeps, and the tier-2
/// SlowSpin / FuaRetry / SlowFua specialists are all just `Linear` at different
/// `params`.
pub(super) struct Linear { pub(super) struct Linear {
pub reverse: bool, pub direction: Direction,
pub fast: bool, pub params: ReadParams,
} }
impl SectionHandler for Linear { impl SectionHandler for Linear {
fn name(&self) -> &'static str { fn name(&self) -> String {
match (self.reverse, self.fast) { format!("linear:{}:{}", self.direction.tag(), self.params.tag())
(true, true) => "linear:reverse:fast",
(true, false) => "linear:reverse:slow",
(false, true) => "linear:forward:fast",
(false, false) => "linear:forward:slow",
}
} }
fn recover( fn recover(
@@ -311,13 +452,13 @@ impl SectionHandler for Linear {
bad: &mut SubRanges, bad: &mut SubRanges,
deadline: Instant, deadline: Instant,
) -> HandlerOutcome { ) -> HandlerOutcome {
let recovery = !self.fast; let reverse = self.direction.is_reverse();
let batch_bytes = BATCH_SECTORS * SECTOR; let batch_bytes = BATCH_SECTORS * SECTOR;
let mut buf = vec![0u8; batch_bytes as usize]; let mut buf = vec![0u8; batch_bytes as usize];
// Snapshot the sub-ranges: we mutate `bad` via remove() as we recover, // Snapshot the sub-ranges: we mutate `bad` via remove() as we recover,
// and iterating the snapshot keeps that from disturbing the walk. // and iterating the snapshot keeps that from disturbing the walk.
let mut snapshot: Vec<(u64, u64)> = bad.ranges().to_vec(); let mut snapshot: Vec<(u64, u64)> = bad.ranges().to_vec();
if self.reverse { if reverse {
snapshot.reverse(); snapshot.reverse();
} }
@@ -332,13 +473,13 @@ impl SectionHandler for Linear {
return HandlerOutcome::Remaining; return HandlerOutcome::Remaining;
} }
let span = batch_bytes.min(rl - done); let span = batch_bytes.min(rl - done);
let pos = if self.reverse { let pos = if reverse {
rp + (rl - done - span) rp + (rl - done - span)
} else { } else {
rp + done rp + done
}; };
let count = (span / SECTOR) as u16; let count = (span / SECTOR) as u16;
match read_span(ctx, &mut buf, pos, count, recovery) { match read_span(ctx, &mut buf, pos, count, self.params) {
ReadHit::Good => bad.remove(pos, span), ReadHit::Good => bad.remove(pos, span),
// Keep reads at the full batch — no per-sector grind (proven // Keep reads at the full batch — no per-sector grind (proven
// worse on the BU40N, and it's what stalled a handler on a // worse on the BU40N, and it's what stalled a handler on a
@@ -366,13 +507,17 @@ impl SectionHandler for Linear {
/// reads. The two failing ends become smaller bad sub-ranges, pushed back to be /// reads. The two failing ends become smaller bad sub-ranges, pushed back to be
/// bisected again. A dead middle just splits into halves. This shreds one huge /// bisected again. A dead middle just splits into halves. This shreds one huge
/// bad range into precisely-located small dead clusters (a handful of sectors) /// bad range into precisely-located small dead clusters (a handful of sectors)
/// instead of leaving the whole thing bad. Uses fast reads: it LOCATES readable /// instead of leaving the whole thing bad. `params` is normally fast reads: it
/// data; deep-recovering the dead sectors is the slow linear handlers' job. /// LOCATES readable data; deep-recovering the dead sectors is the slow linear
pub(super) struct Bisect; /// handlers' job. Tier 2 also runs a Bisect at FUA/deep params to shred islands
/// under cache-bypass.
pub(super) struct Bisect {
pub params: ReadParams,
}
impl SectionHandler for Bisect { impl SectionHandler for Bisect {
fn name(&self) -> &'static str { fn name(&self) -> String {
"bisect" format!("bisect:{}", self.params.tag())
} }
fn recover( fn recover(
@@ -401,7 +546,7 @@ impl SectionHandler for Bisect {
} }
let end = rp + rl; let end = rp + rl;
let mid = rp + (rl / SECTOR / 2) * SECTOR; let mid = rp + (rl / SECTOR / 2) * SECTOR;
match read_span(ctx, &mut probe, mid, 1, false) { match read_span(ctx, &mut probe, mid, 1, self.params) {
ReadHit::Good => { ReadHit::Good => {
bad.remove(mid, SECTOR); bad.remove(mid, SECTOR);
// Expand FORWARD from mid+1 in batches until a read fails. // Expand FORWARD from mid+1 in batches until a read fails.
@@ -416,7 +561,7 @@ impl SectionHandler for Bisect {
} }
let span = step.min(end - fwd); let span = step.min(end - fwd);
let count = (span / SECTOR) as u16; let count = (span / SECTOR) as u16;
match read_span(ctx, &mut buf[..span as usize], fwd, count, false) { match read_span(ctx, &mut buf[..span as usize], fwd, count, self.params) {
ReadHit::Good => { ReadHit::Good => {
bad.remove(fwd, span); bad.remove(fwd, span);
fwd += span; fwd += span;
@@ -448,7 +593,7 @@ impl SectionHandler for Bisect {
let span = step.min(bwd - rp); let span = step.min(bwd - rp);
let pos = bwd - span; let pos = bwd - span;
let count = (span / SECTOR) as u16; let count = (span / SECTOR) as u16;
match read_span(ctx, &mut buf[..span as usize], pos, count, false) { match read_span(ctx, &mut buf[..span as usize], pos, count, self.params) {
ReadHit::Good => { ReadHit::Good => {
bad.remove(pos, span); bad.remove(pos, span);
bwd = pos; bwd = pos;
@@ -509,11 +654,13 @@ impl SectionHandler for Bisect {
/// pass). Without it a linear walk pays one up-to-10 s read per dead batch /// pass). Without it a linear walk pays one up-to-10 s read per dead batch
/// across the whole run, so a deadline-bounded pass never reaches readable data /// across the whole run, so a deadline-bounded pass never reaches readable data
/// buried behind a big dead front (exactly the 192 MB range on Dune). /// buried behind a big dead front (exactly the 192 MB range on Dune).
pub(super) struct Jump; pub(super) struct Jump {
pub params: ReadParams,
}
impl SectionHandler for Jump { impl SectionHandler for Jump {
fn name(&self) -> &'static str { fn name(&self) -> String {
"jump" format!("jump:{}", self.params.tag())
} }
fn recover( fn recover(
@@ -538,7 +685,7 @@ impl SectionHandler for Jump {
let span = batch.min(rl - off); let span = batch.min(rl - off);
let pos = rp + off; let pos = rp + off;
let count = (span / SECTOR) as u16; let count = (span / SECTOR) as u16;
match read_span(ctx, &mut buf[..span as usize], pos, count, false) { match read_span(ctx, &mut buf[..span as usize], pos, count, self.params) {
ReadHit::Good => { ReadHit::Good => {
bad.remove(pos, span); bad.remove(pos, span);
consec_fail = 0; consec_fail = 0;
@@ -583,7 +730,7 @@ impl SectionHandler for Jump {
/// before the ranking narrows to the winners ("try each quick, then prioritise"). /// before the ranking narrows to the winners ("try each quick, then prioritise").
#[derive(Default)] #[derive(Default)]
pub(super) struct HandlerScoreboard { pub(super) struct HandlerScoreboard {
stats: std::collections::HashMap<&'static str, ScoreStat>, stats: std::collections::HashMap<String, ScoreStat>,
} }
#[derive(Default, Clone, Copy)] #[derive(Default, Clone, Copy)]
@@ -603,8 +750,8 @@ impl HandlerScoreboard {
} }
/// Record one attempt: bytes recovered over `elapsed`. /// Record one attempt: bytes recovered over `elapsed`.
fn record(&mut self, name: &'static str, recovered: u64, elapsed: std::time::Duration) { fn record(&mut self, name: &str, recovered: u64, elapsed: std::time::Duration) {
let e = self.stats.entry(name).or_default(); let e = self.stats.entry(name.to_string()).or_default();
e.recovered = e.recovered.saturating_add(recovered); e.recovered = e.recovered.saturating_add(recovered);
e.nanos = e.nanos.saturating_add(elapsed.as_nanos()); e.nanos = e.nanos.saturating_add(elapsed.as_nanos());
e.attempts += 1; e.attempts += 1;
@@ -635,7 +782,7 @@ impl HandlerScoreboard {
tracing::info!( tracing::info!(
target: "freemkv::disc", target: "freemkv::disc",
phase = "scorecard", phase = "scorecard",
handler = *name, handler = name.as_str(),
recovered_mb = s.recovered as f64 / 1_048_576.0, recovered_mb = s.recovered as f64 / 1_048_576.0,
attempts = s.attempts, attempts = s.attempts,
mb_per_s = mbps, mb_per_s = mbps,
@@ -658,11 +805,12 @@ pub(super) fn run_handlers(
section_deadline_for: impl Fn(&SubRanges) -> Instant, section_deadline_for: impl Fn(&SubRanges) -> Instant,
) -> HandlerOutcome { ) -> HandlerOutcome {
// Best-first by recovery rate so far; untried handlers rank top (calibrate). // Best-first by recovery rate so far; untried handlers rank top (calibrate).
handlers.sort_by_key(|h| std::cmp::Reverse(scoreboard.rank(h.name()))); handlers.sort_by_key(|h| std::cmp::Reverse(scoreboard.rank(&h.name())));
for handler in handlers.iter_mut() { for handler in handlers.iter_mut() {
if bad.is_empty() { if bad.is_empty() {
return HandlerOutcome::Complete; return HandlerOutcome::Complete;
} }
let name = handler.name();
let before = bad.total_len(); let before = bad.total_len();
let deadline = section_deadline_for(bad); let deadline = section_deadline_for(bad);
let started = (ctx.now)(); let started = (ctx.now)();
@@ -670,13 +818,20 @@ pub(super) fn run_handlers(
// the early-yield trips. // the early-yield trips.
ctx.unproductive = 0; ctx.unproductive = 0;
let outcome = handler.recover(ctx, bad, deadline); let outcome = handler.recover(ctx, bad, deadline);
// A handler may have dropped the spindle (SlowSpin / SpeedSweep) or set
// FUA; restore max speed before the next handler so it starts from the
// streaming default (FUA is a per-read param, so nothing to unwind there).
if ctx.cur_speed != SPEED_MAX_KBS {
ctx.reader.set_speed(SPEED_MAX_KBS);
ctx.cur_speed = SPEED_MAX_KBS;
}
let elapsed = (ctx.now)().duration_since(started); let elapsed = (ctx.now)().duration_since(started);
let after = bad.total_len(); let after = bad.total_len();
scoreboard.record(handler.name(), before.saturating_sub(after), elapsed); scoreboard.record(&name, before.saturating_sub(after), elapsed);
tracing::info!( tracing::info!(
target: "freemkv::disc", target: "freemkv::disc",
phase = "section_recover.handler", phase = "section_recover.handler",
handler = handler.name(), handler = name.as_str(),
bad_bytes_before = before, bad_bytes_before = before,
bad_bytes_after = after, bad_bytes_after = after,
recovered = before.saturating_sub(after), recovered = before.saturating_sub(after),
@@ -721,19 +876,59 @@ mod tests {
clock_nanos: Arc<AtomicU64>, clock_nanos: Arc<AtomicU64>,
per_read: Duration, per_read: Duration,
reads: Arc<AtomicU64>, reads: Arc<AtomicU64>,
// ── Physical failure-mode models (all default-empty) ─────────────────
// Each conditional sector reads ONLY when the drive state the handler
// manipulates (speed / FUA / approach direction) matches — so a test
// that recovers it PROVES the technique was actually exercised, not that
// a plain read happened to work.
/// Current `SET CD SPEED` value (updated by `set_speed`); max at build.
speed: u16,
/// Reads ONLY at min speed (fails at max) → SlowSpin / SpeedSweep.
slow_only: HashSet<u32>,
/// Reads ONLY on the Nth *physical* (FUA) attempt; a cached (non-FUA)
/// re-read never gets it → FuaRetry. Maps LBA → attempts required.
fua_need: HashMap<u32, u32>,
/// Physical (FUA) attempts observed so far, per LBA.
fua_seen: HashMap<u32, u32>,
/// Reads ONLY when approached from ABOVE (the previous physical access
/// was a higher LBA) → Oscillate's reverse-into pass.
dir_reverse_only: HashSet<u32>,
/// Reads ONLY when the immediately-preceding sector was the previous
/// physical access (PLL/servo primed) → CachePrime.
prime_only: HashSet<u32>,
/// LBA of the last sector physically accessed (success or fail) — the
/// approach-direction / priming signal the specialists drive.
last_lba: Option<u32>,
} }
impl SectorSource for FakeDisc { impl SectorSource for FakeDisc {
fn read_sectors( fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
) -> Result<usize> {
// Bulk (non-FUA) path.
self.read_sectors_fua(lba, count, buf, recovery, false)
}
fn read_sectors_fua(
&mut self, &mut self,
lba: u32, lba: u32,
count: u16, count: u16,
buf: &mut [u8], buf: &mut [u8],
_recovery: bool, _recovery: bool,
fua: bool,
) -> Result<usize> { ) -> Result<usize> {
self.reads.fetch_add(1, Ordering::Relaxed); self.reads.fetch_add(1, Ordering::Relaxed);
self.clock_nanos self.clock_nanos
.fetch_add(self.per_read.as_nanos() as u64, Ordering::Relaxed); .fetch_add(self.per_read.as_nanos() as u64, Ordering::Relaxed);
// The head moved across this span; record where it ended so the NEXT
// read can see the approach direction / priming (both success and
// failure move the head).
let prev = self.last_lba;
self.last_lba = Some(lba + count as u32 - 1);
if let Some(t) = self.transport_at { if let Some(t) = self.transport_at {
if (lba..lba + count as u32).contains(&t) { if (lba..lba + count as u32).contains(&t) {
return Err(Error::ScsiError { return Err(Error::ScsiError {
@@ -767,6 +962,32 @@ mod tests {
sense: None, sense: None,
}); });
} }
// Marginal sector: reads only at min spindle speed.
if self.slow_only.contains(&l) && self.speed != SPEED_MIN_KBS {
return Err(bad_sector(l));
}
// Stochastic sector: needs N physical (FUA) reads; a cached read
// can never land it (cache masks the good re-read).
if let Some(need) = self.fua_need.get(&l).copied() {
if !fua {
return Err(bad_sector(l));
}
let seen = self.fua_seen.entry(l).or_insert(0);
*seen += 1;
if *seen < need {
return Err(bad_sector(l));
}
}
// Direction-dependent tracking: reads only when approached from
// above (previous physical access was a higher LBA).
if self.dir_reverse_only.contains(&l) && prev.is_none_or(|p| p <= l) {
return Err(bad_sector(l));
}
// Boundary sector: reads only when the preceding sector was the
// previous physical access (servo primed).
if self.prime_only.contains(&l) && prev != l.checked_sub(1) {
return Err(bad_sector(l));
}
} }
let bytes = count as usize * SECTOR as usize; let bytes = count as usize * SECTOR as usize;
for (i, b) in buf[..bytes].iter_mut().enumerate() { for (i, b) in buf[..bytes].iter_mut().enumerate() {
@@ -774,6 +995,20 @@ mod tests {
} }
Ok(bytes) Ok(bytes)
} }
fn set_speed(&mut self, kbs: u16) {
self.speed = kbs;
}
}
/// The ordinary recoverable bad-sector error (CHECK CONDITION, no sense) the
/// conditional failure modes return when their precondition isn't met.
fn bad_sector(l: u32) -> Error {
Error::DiscRead {
sector: l as u64,
status: Some(0x02),
sense: None,
}
} }
/// Records every recovered span so a test can assert which sectors came back. /// Records every recovered span so a test can assert which sectors came back.
@@ -805,6 +1040,13 @@ mod tests {
clock_nanos: clock_nanos.clone(), clock_nanos: clock_nanos.clone(),
per_read, per_read,
reads: reads.clone(), reads: reads.clone(),
speed: SPEED_MAX_KBS,
slow_only: HashSet::new(),
fua_need: HashMap::new(),
fua_seen: HashMap::new(),
dir_reverse_only: HashSet::new(),
prime_only: HashSet::new(),
last_lba: None,
}; };
( (
Harness { Harness {
@@ -853,13 +1095,14 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
let mut bad = SubRanges::from_section(0, 10 * SECTOR); let mut bad = SubRanges::from_section(0, 10 * SECTOR);
let deadline = (ctx.now)() + Duration::from_secs(10); let deadline = (ctx.now)() + Duration::from_secs(10);
// Linear leaves the failed 10-sector batch whole. // Linear leaves the failed 10-sector batch whole.
Linear { Linear {
reverse: false, direction: Direction::Forward,
fast: false, params: ReadParams::deep(),
} }
.recover(&mut ctx, &mut bad, deadline); .recover(&mut ctx, &mut bad, deadline);
assert_eq!( assert_eq!(
@@ -869,7 +1112,10 @@ mod tests {
); );
// Bisect salvages the readable sectors around the dead ones. // Bisect salvages the readable sectors around the dead ones.
ctx.unproductive = 0; ctx.unproductive = 0;
let out = Bisect.recover(&mut ctx, &mut bad, deadline); let out = Bisect {
params: ReadParams::fast(),
}
.recover(&mut ctx, &mut bad, deadline);
assert_eq!(out, HandlerOutcome::Remaining); assert_eq!(out, HandlerOutcome::Remaining);
// Exactly the two dead sectors remain. // Exactly the two dead sectors remain.
assert_eq!(bad.total_len(), 2 * SECTOR); assert_eq!(bad.total_len(), 2 * SECTOR);
@@ -902,12 +1148,13 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
let mut bad = SubRanges::from_section(0, 40 * SECTOR); let mut bad = SubRanges::from_section(0, 40 * SECTOR);
let deadline = (ctx.now)() + Duration::from_secs(10); let deadline = (ctx.now)() + Duration::from_secs(10);
let mut lin = Linear { let mut lin = Linear {
reverse: false, direction: Direction::Forward,
fast: false, params: ReadParams::deep(),
}; };
let out = lin.recover(&mut ctx, &mut bad, deadline); let out = lin.recover(&mut ctx, &mut bad, deadline);
assert_eq!(out, HandlerOutcome::Remaining); assert_eq!(out, HandlerOutcome::Remaining);
@@ -940,12 +1187,13 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
let mut bad = SubRanges::from_section(0, 1000 * SECTOR); let mut bad = SubRanges::from_section(0, 1000 * SECTOR);
let deadline = (ctx.now)() + Duration::from_secs(3); let deadline = (ctx.now)() + Duration::from_secs(3);
let mut lin = Linear { let mut lin = Linear {
reverse: false, direction: Direction::Forward,
fast: true, params: ReadParams::fast(),
}; };
let out = lin.recover(&mut ctx, &mut bad, deadline); let out = lin.recover(&mut ctx, &mut bad, deadline);
assert_eq!(out, HandlerOutcome::Remaining); assert_eq!(out, HandlerOutcome::Remaining);
@@ -976,10 +1224,13 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
let mut bad = SubRanges::from_section(0, 9 * SECTOR); let mut bad = SubRanges::from_section(0, 9 * SECTOR);
let deadline = (ctx.now)() + Duration::from_secs(10); let deadline = (ctx.now)() + Duration::from_secs(10);
let mut bis = Bisect; let mut bis = Bisect {
params: ReadParams::fast(),
};
let out = bis.recover(&mut ctx, &mut bad, deadline); let out = bis.recover(&mut ctx, &mut bad, deadline);
assert_eq!(out, HandlerOutcome::Remaining); assert_eq!(out, HandlerOutcome::Remaining);
assert!( assert!(
@@ -1013,18 +1264,21 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
let mut bad = SubRanges::from_section(0, 16 * SECTOR); let mut bad = SubRanges::from_section(0, 16 * SECTOR);
let mut handlers: Vec<Box<dyn SectionHandler>> = vec![ let mut handlers: Vec<Box<dyn SectionHandler>> = vec![
Box::new(Linear { Box::new(Linear {
reverse: true, direction: Direction::Reverse,
fast: false, params: ReadParams::deep(),
}), }),
Box::new(Linear { Box::new(Linear {
reverse: false, direction: Direction::Forward,
fast: false, params: ReadParams::deep(),
}),
Box::new(Bisect {
params: ReadParams::fast(),
}), }),
Box::new(Bisect),
]; ];
let deadline_base = (ctx.now)(); let deadline_base = (ctx.now)();
let mut scoreboard = HandlerScoreboard::default(); let mut scoreboard = HandlerScoreboard::default();
@@ -1055,11 +1309,12 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
let mut bad = SubRanges::from_section(0, 64 * SECTOR); let mut bad = SubRanges::from_section(0, 64 * SECTOR);
let mut handlers: Vec<Box<dyn SectionHandler>> = vec![Box::new(Linear { let mut handlers: Vec<Box<dyn SectionHandler>> = vec![Box::new(Linear {
reverse: false, direction: Direction::Forward,
fast: true, params: ReadParams::fast(),
})]; })];
let base = (ctx.now)(); let base = (ctx.now)();
let mut scoreboard = HandlerScoreboard::default(); let mut scoreboard = HandlerScoreboard::default();
@@ -1087,13 +1342,14 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
// Single-sector batches so the transport LBA is hit directly. // Single-sector batches so the transport LBA is hit directly.
let mut bad = SubRanges::from_section(0, 8 * SECTOR); let mut bad = SubRanges::from_section(0, 8 * SECTOR);
let deadline = (ctx.now)() + Duration::from_secs(10); let deadline = (ctx.now)() + Duration::from_secs(10);
let mut lin = Linear { let mut lin = Linear {
reverse: false, direction: Direction::Forward,
fast: true, params: ReadParams::fast(),
}; };
let out = lin.recover(&mut ctx, &mut bad, deadline); let out = lin.recover(&mut ctx, &mut bad, deadline);
assert_eq!(out, HandlerOutcome::TransportFault); assert_eq!(out, HandlerOutcome::TransportFault);
@@ -1122,21 +1378,26 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
let mut bad = SubRanges::from_section(0, 1000 * SECTOR); let mut bad = SubRanges::from_section(0, 1000 * SECTOR);
// The full tier-0 chain: the wedge streak persists across handlers (only // The full tier-0 chain: the wedge streak persists across handlers (only
// `unproductive` resets per handler), so it reaches the abort threshold // `unproductive` resets per handler), so it reaches the abort threshold
// even though each handler yields early on the dead streak. // even though each handler yields early on the dead streak.
let mut handlers: Vec<Box<dyn SectionHandler>> = vec![ let mut handlers: Vec<Box<dyn SectionHandler>> = vec![
Box::new(Bisect), Box::new(Bisect {
Box::new(Jump), params: ReadParams::fast(),
Box::new(Linear { }),
reverse: true, Box::new(Jump {
fast: true, params: ReadParams::fast(),
}), }),
Box::new(Linear { Box::new(Linear {
reverse: false, direction: Direction::Reverse,
fast: true, params: ReadParams::fast(),
}),
Box::new(Linear {
direction: Direction::Forward,
params: ReadParams::fast(),
}), }),
]; ];
let mut scoreboard = HandlerScoreboard::default(); let mut scoreboard = HandlerScoreboard::default();
@@ -1181,18 +1442,23 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
let mut bad = SubRanges::from_section(0, 1000 * SECTOR); let mut bad = SubRanges::from_section(0, 1000 * SECTOR);
let mut handlers: Vec<Box<dyn SectionHandler>> = vec![ let mut handlers: Vec<Box<dyn SectionHandler>> = vec![
Box::new(Bisect), Box::new(Bisect {
Box::new(Jump), params: ReadParams::fast(),
Box::new(Linear { }),
reverse: true, Box::new(Jump {
fast: true, params: ReadParams::fast(),
}), }),
Box::new(Linear { Box::new(Linear {
reverse: false, direction: Direction::Reverse,
fast: true, params: ReadParams::fast(),
}),
Box::new(Linear {
direction: Direction::Forward,
params: ReadParams::fast(),
}), }),
]; ];
let mut scoreboard = HandlerScoreboard::default(); let mut scoreboard = HandlerScoreboard::default();
@@ -1237,6 +1503,7 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: carried, wedge_streak: carried,
cur_speed: SPEED_MAX_KBS,
}; };
// Distinct 100-sector section per iteration, all within the wedge set. // Distinct 100-sector section per iteration, all within the wedge set.
let pos = (section as u64) * 100 * SECTOR; let pos = (section as u64) * 100 * SECTOR;
@@ -1244,12 +1511,12 @@ mod tests {
// Tier-1 shape: two slow Linear handlers, nothing that reaches 16 alone. // Tier-1 shape: two slow Linear handlers, nothing that reaches 16 alone.
let mut handlers: Vec<Box<dyn SectionHandler>> = vec![ let mut handlers: Vec<Box<dyn SectionHandler>> = vec![
Box::new(Linear { Box::new(Linear {
reverse: true, direction: Direction::Reverse,
fast: false, params: ReadParams::deep(),
}), }),
Box::new(Linear { Box::new(Linear {
reverse: false, direction: Direction::Forward,
fast: false, params: ReadParams::deep(),
}), }),
]; ];
let mut sb = HandlerScoreboard::default(); let mut sb = HandlerScoreboard::default();
@@ -1288,12 +1555,13 @@ mod tests {
tick: None, tick: None,
unproductive: 0, unproductive: 0,
wedge_streak: 0, wedge_streak: 0,
cur_speed: SPEED_MAX_KBS,
}; };
let mut bad = SubRanges::from_section(0, 100 * SECTOR); let mut bad = SubRanges::from_section(0, 100 * SECTOR);
let deadline = (ctx.now)() + Duration::from_secs(10); let deadline = (ctx.now)() + Duration::from_secs(10);
let mut lin = Linear { let mut lin = Linear {
reverse: false, direction: Direction::Forward,
fast: true, params: ReadParams::fast(),
}; };
let out = lin.recover(&mut ctx, &mut bad, deadline); let out = lin.recover(&mut ctx, &mut bad, deadline);
assert_eq!(out, HandlerOutcome::Halted); assert_eq!(out, HandlerOutcome::Halted);
+62 -13
View File
@@ -658,6 +658,25 @@ impl Drive {
/// recovery layers (Disc::patch multi-pass, DiscStream batch halving) /// recovery layers (Disc::patch multi-pass, DiscStream batch halving)
/// do not touch the wedge-prone reset path. /// do not touch the wedge-prone reset path.
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool) -> Result<usize> { pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool) -> Result<usize> {
// Bulk path: FUA off (the drive cache IS the streaming throughput).
self.read_fua(lba, count, buf, recovery, false)
}
/// [`read`], but with an explicit Force Unit Access request: `fua = true`
/// sets the READ(10) FUA bit so the drive re-fetches the medium instead of
/// returning a cached copy — the Pass-N marginal-sector lever (see
/// [`crate::sector::SectorSource::read_sectors_fua`]). The bulk sweep always
/// passes `false`; only a per-sector recovery handler asks for FUA.
///
/// [`read`]: Drive::read
pub fn read_fua(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
fua: bool,
) -> Result<usize> {
let timeout_ms = if recovery { let timeout_ms = if recovery {
crate::scsi::READ_RECOVERY_TIMEOUT_MS crate::scsi::READ_RECOVERY_TIMEOUT_MS
} else { } else {
@@ -683,7 +702,7 @@ impl Drive {
// read_one call with no behavior change. // read_one call with no behavior change.
let max_sectors = (self.scsi.max_transfer_bytes() / 2048).max(1) as u32; let max_sectors = (self.scsi.max_transfer_bytes() / 2048).max(1) as u32;
if count as u32 <= max_sectors { if count as u32 <= max_sectors {
return self.read_one(lba, count, buf, timeout_ms, recovery); return self.read_one(lba, count, buf, timeout_ms, recovery, fua);
} }
// Large read: split into chunks of at most `max_sectors` sectors, // Large read: split into chunks of at most `max_sectors` sectors,
@@ -699,7 +718,7 @@ impl Drive {
let byte_off = done as usize * 2048; let byte_off = done as usize * 2048;
let byte_len = chunk as usize * 2048; let byte_len = chunk as usize * 2048;
let slice = &mut buf[byte_off..byte_off + byte_len]; let slice = &mut buf[byte_off..byte_off + byte_len];
let n = self.read_one(cur_lba, chunk as u16, slice, timeout_ms, recovery)?; let n = self.read_one(cur_lba, chunk as u16, slice, timeout_ms, recovery, fua)?;
total += n; total += n;
done += chunk; done += chunk;
} }
@@ -722,20 +741,20 @@ impl Drive {
// `recovery` gates only the Linux /dev/sr0 pread fallback below; on // `recovery` gates only the Linux /dev/sr0 pread fallback below; on
// other platforms it is intentionally unused. // other platforms it is intentionally unused.
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] recovery: bool, #[cfg_attr(not(target_os = "linux"), allow(unused_variables))] recovery: bool,
// FUA (Force Unit Access): when set, byte-1 bit 0x08 forces the drive to
// re-fetch the medium past its cache.
fua: bool,
) -> Result<usize> { ) -> Result<usize> {
// FUA (Force Unit Access) is DISABLED for now — byte-1 bit 0x08 cleared. // FUA is OFF on the bulk path — unconditionally forcing every READ(10)
// Unconditionally forcing every READ(10) past the drive cache disabled // past the cache disabled the drive's readahead/streaming cache on the
// the drive's readahead/streaming cache on the bulk sequential sweep and // sequential sweep and collapsed throughput ~10x (UHD 15-25 → ~2 MB/s,
// collapsed throughput ~10x (UHD 15-25 → ~2 MB/s, DVD → ~0.5 MB/s), // DVD → ~0.5 MB/s), disc-type-agnostic — the cache IS the streaming
// disc-type-agnostic — the cache IS the streaming throughput. // throughput. It is set ONLY when a Pass-N recovery handler (FuaRetry)
// // asks for it per marginal-sector re-read, where cache-masking of a
// TODO(#55): reintroduce FUA as a dedicated Pass-N recovery HANDLER that // stochastic sector actually matters (#55).
// sets/clears it per marginal-sector re-read (where cache-masking of a
// stochastic sector actually matters), never blanket-applied to the bulk
// read path.
let cdb = [ let cdb = [
crate::scsi::SCSI_READ_10, crate::scsi::SCSI_READ_10,
0x00, if fua { 0x08 } else { 0x00 },
(lba >> 24) as u8, (lba >> 24) as u8,
(lba >> 16) as u8, (lba >> 16) as u8,
(lba >> 8) as u8, (lba >> 8) as u8,
@@ -1021,6 +1040,17 @@ impl SectorSource for Drive {
self.read(lba, count, buf, recovery) self.read(lba, count, buf, recovery)
} }
fn read_sectors_fua(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
fua: bool,
) -> Result<usize> {
self.read_fua(lba, count, buf, recovery, fua)
}
fn set_speed(&mut self, kbs: u16) { fn set_speed(&mut self, kbs: u16) {
Drive::set_speed(self, kbs); Drive::set_speed(self, kbs);
} }
@@ -1437,6 +1467,25 @@ mod command_tests {
assert_eq!(&c[7..9], &[0x00, 0x02], "transfer length big-endian"); assert_eq!(&c[7..9], &[0x00, 0x02], "transfer length big-endian");
} }
#[test]
fn read_fua_sets_the_force_unit_access_bit() {
// The Pass-N FuaRetry lever: read_fua(.., fua=true) sets READ(10) byte-1
// bit 0x08 so the drive re-fetches the medium past its cache; fua=false
// leaves it clear (the bulk path).
let RecordingHarness {
drive: mut d,
cdb,
timeouts: _to,
} = recording(TransportOutcome::Ok(2048));
let mut buf = vec![0u8; 2048];
d.read_fua(0, 1, &mut buf, false, true).unwrap();
assert_eq!(
cdb.lock().unwrap()[1],
0x08,
"FUA requested — byte-1 bit 0x08 set so the drive bypasses its cache"
);
}
#[test] #[test]
fn read_recovery_flag_selects_60s_timeout() { fn read_recovery_flag_selects_60s_timeout() {
// recovery=true must use READ_RECOVERY_TIMEOUT_MS (60 s); false // recovery=true must use READ_RECOVERY_TIMEOUT_MS (60 s); false
+17 -1
View File
@@ -470,6 +470,20 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
count: u16, count: u16,
buf: &mut [u8], buf: &mut [u8],
recovery: bool, recovery: bool,
) -> Result<usize> {
// Bulk path: no Force Unit Access (the cache IS the streaming
// throughput). FUA is a Pass-N recovery lever threaded through
// `read_sectors_fua`.
self.read_sectors_fua(lba, count, buf, recovery, false)
}
fn read_sectors_fua(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
fua: bool,
) -> Result<usize> { ) -> Result<usize> {
// Defense-in-depth: AACS aligned units are 3 sectors (6144 bytes) and // Defense-in-depth: AACS aligned units are 3 sectors (6144 bytes) and
// `decrypt_sectors` anchors units at buffer offset 0. A read that does // `decrypt_sectors` anchors units at buffer offset 0. A read that does
@@ -488,7 +502,9 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
return Err(crate::error::Error::DecryptFailed); return Err(crate::error::Error::DecryptFailed);
} }
let read_t0 = std::time::Instant::now(); let read_t0 = std::time::Instant::now();
let n = self.inner.read_sectors(lba, count, buf, recovery)?; let n = self
.inner
.read_sectors_fua(lba, count, buf, recovery, fua)?;
let read_ms = read_t0.elapsed().as_millis() as u64; let read_ms = read_t0.elapsed().as_millis() as u64;
// Decrypt the bytes just read. Scheme-agnostic: `decrypt_sectors*` // Decrypt the bytes just read. Scheme-agnostic: `decrypt_sectors*`
// dispatches on the keys (None / CSS / AACS) and returns the count of // dispatches on the keys (None / CSS / AACS) and returns the count of
+49
View File
@@ -53,6 +53,33 @@ pub trait SectorSource: Send {
recovery: bool, recovery: bool,
) -> Result<usize>; ) -> Result<usize>;
/// Like [`read_sectors`], but with an explicit Force Unit Access request.
///
/// `fua = true` asks the drive to bypass its readahead cache and physically
/// re-fetch the medium — a Pass-N marginal-sector lever: a cached hit can
/// mask a *stochastic* sector that would land differently off the platter on
/// each physical read, so FuaRetry re-reads it FUA. It is never blanket-
/// applied to the bulk path (forcing every sequential read past the cache
/// collapses streaming throughput ~10×).
///
/// The default ignores `fua` and delegates to [`read_sectors`]: only a live
/// [`Drive`] sets the CDB bit; file- / memory-backed sources have no drive
/// cache to bypass, so FUA is meaningless to them.
///
/// [`read_sectors`]: SectorSource::read_sectors
/// [`Drive`]: crate::drive::Drive
fn read_sectors_fua(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
fua: bool,
) -> Result<usize> {
let _ = fua;
self.read_sectors(lba, count, buf, recovery)
}
/// Optional speed control for sources that map to a physical /// Optional speed control for sources that map to a physical
/// drive. No-op for everything else. /// drive. No-op for everything else.
fn set_speed(&mut self, _kbs: u16) {} fn set_speed(&mut self, _kbs: u16) {}
@@ -87,6 +114,17 @@ impl SectorSource for Box<dyn SectorSource> {
(**self).read_sectors(lba, count, buf, recovery) (**self).read_sectors(lba, count, buf, recovery)
} }
fn read_sectors_fua(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
fua: bool,
) -> Result<usize> {
(**self).read_sectors_fua(lba, count, buf, recovery, fua)
}
fn set_speed(&mut self, kbs: u16) { fn set_speed(&mut self, kbs: u16) {
(**self).set_speed(kbs) (**self).set_speed(kbs)
} }
@@ -107,6 +145,17 @@ impl SectorSource for &mut (dyn SectorSource + '_) {
(**self).read_sectors(lba, count, buf, recovery) (**self).read_sectors(lba, count, buf, recovery)
} }
fn read_sectors_fua(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
recovery: bool,
fua: bool,
) -> Result<usize> {
(**self).read_sectors_fua(lba, count, buf, recovery, fua)
}
fn set_speed(&mut self, kbs: u16) { fn set_speed(&mut self, kbs: u16) {
(**self).set_speed(kbs) (**self).set_speed(kbs)
} }