recovery: wire tier 2 marginal specialists into the Pass-N chain
Add a third breadth-first tier (PATCH_TIERS 2->3) that runs the marginal specialists on the hardened residual tiers 0-1 leave: SlowSpin (Linear fwd+rev @ min), FuaRetry (Linear fwd+rev+Bisect @ FUA), SlowFua (Linear @ min+FUA), CachePrime, Oscillate (@ max and @ min), SpeedSweep. Every read is a wedge-safe read_span, so they inherit wedge-abort / unproductive-yield / deadline for free. All are new configs, so the EWMA scorecard calibrates each once then ranks by decayed rate — a specialist that doesn't fit self- deprioritises. Tiers 0-1 (fast scouts, slow-deep) are unchanged; this is purely additive. Also switch the scorecard log sort to sort_by_key. cargo test -p libfreemkv green (2200 passed).
This commit is contained in:
+82
-7
@@ -59,8 +59,9 @@ 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, Direction, HandlerCtx, HandlerOutcome, HandlerScoreboard, Jump, Linear, ReadParams,
|
Bisect, CachePrime, Direction, HandlerCtx, HandlerOutcome, HandlerScoreboard, Jump, Linear,
|
||||||
RecoverySink, SectionHandler, run_handlers,
|
Oscillate, ReadParams, RecoverySink, SectionHandler, SpeedPref, SpeedSweep, TimeoutPref,
|
||||||
|
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
|
||||||
@@ -349,8 +350,10 @@ use crate::io::pipeline::Pipeline;
|
|||||||
use crate::sector::SectorSource;
|
use crate::sector::SectorSource;
|
||||||
|
|
||||||
/// Breadth-first recovery tiers. Tier 0 fast-sweeps every bad range; tier 1
|
/// Breadth-first recovery tiers. Tier 0 fast-sweeps every bad range; tier 1
|
||||||
/// deep-recovers the residual. See `PatchCtx::run`.
|
/// deep-recovers the residual; tier 2 runs the marginal specialists on whatever
|
||||||
const PATCH_TIERS: usize = 2;
|
/// tiers 0-1 leave (the true hardened residual). See `PatchCtx::run` and
|
||||||
|
/// `build_tier_handlers`.
|
||||||
|
const PATCH_TIERS: usize = 3;
|
||||||
|
|
||||||
/// Send a `PatchItem` and translate a `SendError` (consumer thread died
|
/// Send a `PatchItem` and translate a `SendError` (consumer thread died
|
||||||
/// / panicked) into a library error so the caller propagates cleanly.
|
/// / panicked) into a library error so the caller propagates cleanly.
|
||||||
@@ -448,7 +451,13 @@ 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_fua(aligned_lba, aligned_count as u16, &mut scratch, recovery, fua)?;
|
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 {
|
||||||
@@ -790,8 +799,74 @@ fn build_tier_handlers(tier: usize) -> Vec<Box<dyn SectionHandler>> {
|
|||||||
params: ReadParams::deep(),
|
params: ReadParams::deep(),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
// Tier 2 — marginal specialists (wired in when PATCH_TIERS reaches 3).
|
// Tier 2 — marginal specialists, run ONLY on the hardened residual that
|
||||||
_ => Vec::new(),
|
// tiers 0-1 leave. Each targets ONE physical failure mode. They are all
|
||||||
|
// NEW configs, so the scorecard calibrates each once then ranks by its
|
||||||
|
// decayed rate — a specialist that doesn't fit THIS disc self-
|
||||||
|
// deprioritises (scores low, yields after 4 unproductive reads) and one
|
||||||
|
// that starts landing sectors climbs. Every read is a wedge-safe
|
||||||
|
// `read_span`, so they inherit the wedge-abort / unproductive-yield /
|
||||||
|
// deadline bounds for free. Additive: tiers 0-1 are untouched.
|
||||||
|
_ => {
|
||||||
|
// Slower spindle (more servo dwell + ECC integration per sector).
|
||||||
|
let min_deep = ReadParams {
|
||||||
|
speed: SpeedPref::Min,
|
||||||
|
fua: false,
|
||||||
|
timeout: TimeoutPref::Deep,
|
||||||
|
};
|
||||||
|
// Cache-bypass physical re-read (stochastic marginal sectors).
|
||||||
|
let fua_deep = ReadParams {
|
||||||
|
speed: SpeedPref::Max,
|
||||||
|
fua: true,
|
||||||
|
timeout: TimeoutPref::Deep,
|
||||||
|
};
|
||||||
|
// Both levers for the hardest sectors (min spindle AND cache-bypass).
|
||||||
|
let slow_fua = ReadParams {
|
||||||
|
speed: SpeedPref::Min,
|
||||||
|
fua: true,
|
||||||
|
timeout: TimeoutPref::Deep,
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
// SlowSpin: Linear fwd + rev at min speed.
|
||||||
|
Box::new(Linear {
|
||||||
|
direction: Direction::Reverse,
|
||||||
|
params: min_deep,
|
||||||
|
}),
|
||||||
|
Box::new(Linear {
|
||||||
|
direction: Direction::Forward,
|
||||||
|
params: min_deep,
|
||||||
|
}),
|
||||||
|
// FuaRetry: Linear fwd + rev + Bisect under FUA (multiple physical
|
||||||
|
// attempts per marginal sector).
|
||||||
|
Box::new(Linear {
|
||||||
|
direction: Direction::Forward,
|
||||||
|
params: fua_deep,
|
||||||
|
}),
|
||||||
|
Box::new(Linear {
|
||||||
|
direction: Direction::Reverse,
|
||||||
|
params: fua_deep,
|
||||||
|
}),
|
||||||
|
Box::new(Bisect { params: fua_deep }),
|
||||||
|
// SlowFua: the hardest sector — min speed AND FUA.
|
||||||
|
Box::new(Linear {
|
||||||
|
direction: Direction::Forward,
|
||||||
|
params: slow_fua,
|
||||||
|
}),
|
||||||
|
// CachePrime: warm the channel on the preceding good run first.
|
||||||
|
Box::new(CachePrime {
|
||||||
|
params: ReadParams::deep(),
|
||||||
|
}),
|
||||||
|
// Oscillate: alternate approach direction, at max and at min.
|
||||||
|
Box::new(Oscillate {
|
||||||
|
params: ReadParams::deep(),
|
||||||
|
}),
|
||||||
|
Box::new(Oscillate { params: min_deep }),
|
||||||
|
// SpeedSweep: per-sector Max→Min speed search.
|
||||||
|
Box::new(SpeedSweep {
|
||||||
|
params: ReadParams::deep(),
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -821,7 +821,8 @@ impl SectionHandler for CachePrime {
|
|||||||
// the servo/PLL, so the boundary sector is read warm, not cold-seeked.
|
// the servo/PLL, so the boundary sector is read warm, not cold-seeked.
|
||||||
if rp >= SECTOR {
|
if rp >= SECTOR {
|
||||||
// A bad/absent preceding sector just means no prime — read cold.
|
// A bad/absent preceding sector just means no prime — read cold.
|
||||||
if let ReadHit::Transport = read_span(ctx, &mut prime, rp - SECTOR, 1, self.params) {
|
if let ReadHit::Transport = read_span(ctx, &mut prime, rp - SECTOR, 1, self.params)
|
||||||
|
{
|
||||||
return HandlerOutcome::TransportFault;
|
return HandlerOutcome::TransportFault;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,7 +1019,7 @@ impl HandlerScoreboard {
|
|||||||
pub(super) fn log(&self) {
|
pub(super) fn log(&self) {
|
||||||
let mut rows: Vec<_> = self.stats.iter().collect();
|
let mut rows: Vec<_> = self.stats.iter().collect();
|
||||||
// Rank by the decayed rate (the live signal), highest first.
|
// Rank by the decayed rate (the live signal), highest first.
|
||||||
rows.sort_by(|a, b| self.rank(b.0).cmp(&self.rank(a.0)));
|
rows.sort_by_key(|(name, _)| std::cmp::Reverse(self.rank(name)));
|
||||||
for (name, s) in rows {
|
for (name, s) in rows {
|
||||||
let mbps = s.recovered as f64 / (s.nanos as f64 / 1e9).max(1e-9) / 1_048_576.0;
|
let mbps = s.recovered as f64 / (s.nanos as f64 / 1e9).max(1e-9) / 1_048_576.0;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
@@ -1895,7 +1896,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.recover(&mut ctx, &mut bad, deadline);
|
.recover(&mut ctx, &mut bad, deadline);
|
||||||
assert_eq!(out, HandlerOutcome::Remaining);
|
assert_eq!(out, HandlerOutcome::Remaining);
|
||||||
assert_eq!(bad.total_len(), SECTOR, "max-speed linear must leave it bad");
|
assert_eq!(
|
||||||
|
bad.total_len(),
|
||||||
|
SECTOR,
|
||||||
|
"max-speed linear must leave it bad"
|
||||||
|
);
|
||||||
|
|
||||||
// SlowSpin = Linear at min speed — recovers it.
|
// SlowSpin = Linear at min speed — recovers it.
|
||||||
let out = Linear {
|
let out = Linear {
|
||||||
@@ -2012,7 +2017,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.recover(&mut ctx, &mut bad, deadline);
|
.recover(&mut ctx, &mut bad, deadline);
|
||||||
assert_eq!(out, HandlerOutcome::Remaining);
|
assert_eq!(out, HandlerOutcome::Remaining);
|
||||||
assert_eq!(bad.total_len(), SECTOR, "max+fua must miss the min-only sector");
|
assert_eq!(
|
||||||
|
bad.total_len(),
|
||||||
|
SECTOR,
|
||||||
|
"max+fua must miss the min-only sector"
|
||||||
|
);
|
||||||
|
|
||||||
// Min speed but cached (no FUA) → no physical attempt, fails.
|
// Min speed but cached (no FUA) → no physical attempt, fails.
|
||||||
let out = Linear {
|
let out = Linear {
|
||||||
@@ -2021,7 +2030,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.recover(&mut ctx, &mut bad, deadline);
|
.recover(&mut ctx, &mut bad, deadline);
|
||||||
assert_eq!(out, HandlerOutcome::Remaining);
|
assert_eq!(out, HandlerOutcome::Remaining);
|
||||||
assert_eq!(bad.total_len(), SECTOR, "min+cached must miss the FUA-only sector");
|
assert_eq!(
|
||||||
|
bad.total_len(),
|
||||||
|
SECTOR,
|
||||||
|
"min+cached must miss the FUA-only sector"
|
||||||
|
);
|
||||||
|
|
||||||
// Both levers: min speed AND FUA → recovers.
|
// Both levers: min speed AND FUA → recovers.
|
||||||
let out = Linear {
|
let out = Linear {
|
||||||
@@ -2034,7 +2047,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.recover(&mut ctx, &mut bad, deadline);
|
.recover(&mut ctx, &mut bad, deadline);
|
||||||
assert_eq!(out, HandlerOutcome::Complete);
|
assert_eq!(out, HandlerOutcome::Complete);
|
||||||
assert!(bad.is_empty(), "SlowFua (min+fua) must recover the hardest sector");
|
assert!(
|
||||||
|
bad.is_empty(),
|
||||||
|
"SlowFua (min+fua) must recover the hardest sector"
|
||||||
|
);
|
||||||
assert_eq!(sink.got.get(&(11 * SECTOR)).copied(), Some(SECTOR as usize));
|
assert_eq!(sink.got.get(&(11 * SECTOR)).copied(), Some(SECTOR as usize));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2068,7 +2084,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.recover(&mut ctx, &mut bad, deadline);
|
.recover(&mut ctx, &mut bad, deadline);
|
||||||
assert_eq!(out, HandlerOutcome::Complete);
|
assert_eq!(out, HandlerOutcome::Complete);
|
||||||
assert!(bad.is_empty(), "Oscillate must recover the direction-dependent sector");
|
assert!(
|
||||||
|
bad.is_empty(),
|
||||||
|
"Oscillate must recover the direction-dependent sector"
|
||||||
|
);
|
||||||
assert_eq!(sink.got.get(&(13 * SECTOR)).copied(), Some(SECTOR as usize));
|
assert_eq!(sink.got.get(&(13 * SECTOR)).copied(), Some(SECTOR as usize));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2094,7 +2113,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.recover(&mut ctx, &mut bad, deadline);
|
.recover(&mut ctx, &mut bad, deadline);
|
||||||
assert_eq!(out, HandlerOutcome::Remaining);
|
assert_eq!(out, HandlerOutcome::Remaining);
|
||||||
assert_eq!(bad.total_len(), SECTOR, "cold linear must miss the boundary sector");
|
assert_eq!(
|
||||||
|
bad.total_len(),
|
||||||
|
SECTOR,
|
||||||
|
"cold linear must miss the boundary sector"
|
||||||
|
);
|
||||||
|
|
||||||
// CachePrime reads the preceding run first → warm channel → lands it.
|
// CachePrime reads the preceding run first → warm channel → lands it.
|
||||||
let out = CachePrime {
|
let out = CachePrime {
|
||||||
@@ -2102,7 +2125,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
.recover(&mut ctx, &mut bad, deadline);
|
.recover(&mut ctx, &mut bad, deadline);
|
||||||
assert_eq!(out, HandlerOutcome::Complete);
|
assert_eq!(out, HandlerOutcome::Complete);
|
||||||
assert!(bad.is_empty(), "CachePrime must recover the primed boundary sector");
|
assert!(
|
||||||
|
bad.is_empty(),
|
||||||
|
"CachePrime must recover the primed boundary sector"
|
||||||
|
);
|
||||||
assert_eq!(sink.got.get(&(15 * SECTOR)).copied(), Some(SECTOR as usize));
|
assert_eq!(sink.got.get(&(15 * SECTOR)).copied(), Some(SECTOR as usize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user