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:
+71
-41
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user