use crate::error::Result;
use crate::event::{Event, EventKind};
use crate::halt::Halt;
use crate::sector::SectorSource;
use crossbeam_channel::{Receiver, Sender, bounded};
use std::thread::JoinHandle;
pub type EventFn = Box<dyn Fn(Event) + Send + 'static>;
const PREFETCH_CHANNEL_DEPTH: usize = 2;
const SECTOR_ALIGNMENT: u16 = 3;
pub type Batch = std::result::Result<Vec<u8>, std::io::Error>;
pub struct PrefetchedSectorSource {
rx: Receiver<Batch>,
recycle_tx: Sender<Vec<u8>>,
producer: Option<JoinHandle<()>>,
total_sectors: u32,
}
impl PrefetchedSectorSource {
pub fn new<S>(
reader: S,
extents: Vec<crate::disc::Extent>,
batch_sectors: u16,
halt: Option<Halt>,
) -> Result<Self>
where
S: SectorSource + Send + 'static,
{
Self::new_with_events(reader, extents, batch_sectors, halt, None)
}
pub fn new_with_events<S>(
mut reader: S,
extents: Vec<crate::disc::Extent>,
batch_sectors: u16,
halt: Option<Halt>,
event_fn: Option<EventFn>,
) -> Result<Self>
where
S: SectorSource + Send + 'static,
{
if batch_sectors == 0 {
return Err(crate::error::Error::IoError {
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
});
}
let total_sectors: u32 = extents
.iter()
.map(|e| e.sector_count as u64)
.sum::<u64>()
.min(u32::MAX as u64) as u32;
let bytes_total_extents: u64 = extents.iter().map(|e| e.sector_count as u64 * 2048).sum();
let (tx, rx) = bounded::<Batch>(PREFETCH_CHANNEL_DEPTH);
let (recycle_tx, recycle_rx) = bounded::<Vec<u8>>(PREFETCH_CHANNEL_DEPTH + 1);
let batch_bytes = batch_sectors as usize * 2048;
for _ in 0..(PREFETCH_CHANNEL_DEPTH + 1) {
let _ = recycle_tx.send(vec![0u8; batch_bytes]);
}
let producer = std::thread::Builder::new()
.name("freemkv-prefetch".into())
.spawn(move || {
let mut ext_idx = 0usize;
let mut offset: u32 = 0;
let mut bytes_read_total: u64 = 0;
while ext_idx < extents.len() {
if halt.as_ref().map(|h| h.is_cancelled()).unwrap_or(false) {
return;
}
let extent = &extents[ext_idx];
let remaining = extent.sector_count.saturating_sub(offset);
if remaining == 0 {
ext_idx += 1;
offset = 0;
continue;
}
if remaining % SECTOR_ALIGNMENT as u32 != 0
&& remaining < SECTOR_ALIGNMENT as u32
{
let _ = tx.send(Err(crate::error::Error::ExtentNotUnitAligned.into()));
return;
}
let mut sectors = remaining.min(batch_sectors as u32) as u16;
if sectors >= SECTOR_ALIGNMENT {
sectors -= sectors % SECTOR_ALIGNMENT;
} else {
sectors = SECTOR_ALIGNMENT;
}
let bytes = sectors as usize * 2048;
let mut buf = match recycle_rx.recv() {
Ok(b) => b,
Err(_) => return, };
if bytes <= buf.capacity() {
debug_assert!(bytes <= buf.capacity(), "set_len exceeds capacity");
unsafe { buf.set_len(bytes) };
} else {
buf.resize(bytes, 0);
}
let lba = extent.start_lba.saturating_add(offset);
match reader.read_sectors(lba, sectors, &mut buf[..bytes], false) {
Ok(n) => {
if n % 2048 != 0 {
let _ =
tx.send(Err(crate::error::Error::ExtentNotUnitAligned.into()));
return;
}
let sectors_read = (n / 2048) as u32;
buf.truncate(n);
bytes_read_total = bytes_read_total.saturating_add(n as u64);
if let Some(ref f) = event_fn {
f(Event {
kind: EventKind::BytesRead {
bytes: bytes_read_total,
total: bytes_total_extents,
},
});
}
if tx.send(Ok(buf)).is_err() {
return; }
if sectors_read == 0 {
return;
}
offset = offset.saturating_add(sectors_read);
}
Err(e) => {
let _ = tx.send(Err(e.into()));
return;
}
}
}
})
.map_err(|e| crate::error::Error::IoError { source: e })?;
Ok(Self {
rx,
recycle_tx,
producer: Some(producer),
total_sectors,
})
}
pub fn into_channels(self) -> (Receiver<Batch>, Sender<Vec<u8>>, PrefetchShell) {
let total = self.total_sectors;
let me = std::mem::ManuallyDrop::new(self);
let producer = unsafe { std::ptr::read(&me.producer) };
let rx = unsafe { std::ptr::read(&me.rx) };
let recycle = unsafe { std::ptr::read(&me.recycle_tx) };
(rx, recycle, PrefetchShell { producer, total })
}
}
pub struct PrefetchShell {
producer: Option<JoinHandle<()>>,
#[allow(dead_code)]
total: u32,
}
impl Drop for PrefetchShell {
fn drop(&mut self) {
if let Some(h) = self.producer.take() {
let _ = h.join();
}
}
}
impl Drop for PrefetchedSectorSource {
fn drop(&mut self) {
if let Some(h) = self.producer.take() {
let _ = h.join();
}
}
}
impl SectorSource for PrefetchedSectorSource {
fn capacity_sectors(&self) -> u32 {
self.total_sectors
}
fn read_sectors(
&mut self,
_lba: u32,
_count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
match self.rx.recv() {
Ok(Ok(filled)) => {
if filled.len() > buf.len() {
return Err(crate::error::Error::IoError {
source: std::io::Error::from(std::io::ErrorKind::InvalidInput),
});
}
let n = filled.len();
buf[..n].copy_from_slice(&filled[..n]);
let _ = self.recycle_tx.send(filled);
Ok(n)
}
Ok(Err(e)) => Err(crate::error::Error::IoError { source: e }),
Err(_) => Ok(0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::disc::Extent;
use crate::error::Result;
use std::sync::mpsc;
use std::time::Duration;
struct EndlessZeroSource;
impl SectorSource for EndlessZeroSource {
fn read_sectors(
&mut self,
_lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
buf[..bytes].fill(0);
Ok(bytes)
}
}
struct PatternSource {
capacity: u32,
}
impl SectorSource for PatternSource {
fn capacity_sectors(&self) -> u32 {
self.capacity
}
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let bytes = count as usize * 2048;
for s in 0..count as usize {
let base = s * 2048;
let tag = (lba.wrapping_add(s as u32) & 0xff) as u8;
for b in &mut buf[base..base + 2048] {
*b = tag;
}
}
Ok(bytes)
}
}
struct ShortFirstSource {
capacity: u32,
first: bool,
}
impl SectorSource for ShortFirstSource {
fn capacity_sectors(&self) -> u32 {
self.capacity
}
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let give = if self.first {
self.first = false;
SECTOR_ALIGNMENT.min(count)
} else {
count
};
let bytes = give as usize * 2048;
for s in 0..give as usize {
let base = s * 2048;
let tag = (lba.wrapping_add(s as u32) & 0xff) as u8;
for b in &mut buf[base..base + 2048] {
*b = tag;
}
}
Ok(bytes)
}
}
fn big_extent() -> Vec<Extent> {
vec![Extent {
start_lba: 0,
sector_count: u32::MAX,
}]
}
fn within<F: FnOnce() + Send + 'static>(secs: u64, f: F) {
let (done_tx, done_rx) = bounded::<()>(1);
std::thread::spawn(move || {
f();
let _ = done_tx.send(());
});
assert!(
done_rx
.recv_timeout(std::time::Duration::from_secs(secs))
.is_ok(),
"operation did not complete within {secs}s (deadlock)"
);
}
fn with_watchdog<F>(timeout: Duration, f: F)
where
F: FnOnce() + Send + 'static,
{
let (done_tx, done_rx) = mpsc::channel::<()>();
let h = std::thread::spawn(move || {
f();
let _ = done_tx.send(());
});
match done_rx.recv_timeout(timeout) {
Ok(()) => {
let _ = h.join();
}
Err(_) => panic!("watchdog timeout — likely deadlock/hang in prefetch read path"),
}
}
#[test]
fn into_channels_drop_releases_producer() {
within(10, || {
let src = PrefetchedSectorSource::new(EndlessZeroSource, big_extent(), 3, None)
.expect("spawn");
let (rx, recycle_tx, shell) = src.into_channels();
drop(rx);
drop(recycle_tx);
drop(shell);
});
}
#[test]
fn halt_releases_producer() {
within(10, || {
let halt = Halt::new();
let src =
PrefetchedSectorSource::new(EndlessZeroSource, big_extent(), 3, Some(halt.clone()))
.expect("spawn");
let (rx, recycle_tx, shell) = src.into_channels();
let drainer = std::thread::spawn(move || {
while let Ok(item) = rx.recv() {
if let Ok(buf) = item {
let _ = recycle_tx.send(buf);
}
}
});
halt.cancel();
drop(shell);
let _ = drainer.join();
});
}
#[test]
fn zero_batch_rejected() {
let err = PrefetchedSectorSource::new(EndlessZeroSource, big_extent(), 0, None);
assert!(err.is_err(), "zero batch_sectors must be rejected");
}
#[test]
fn direct_reads_past_pool_depth_do_not_deadlock() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![Extent {
start_lba: 0,
sector_count: 24,
}];
let src = PatternSource { capacity: 24 };
let mut pf = PrefetchedSectorSource::new(src, extents, 3, None).expect("spawn");
let mut buf = vec![0u8; 3 * 2048];
let mut total = 0usize;
for _ in 0..16 {
let n = pf.read_sectors(0, 3, &mut buf, false).unwrap();
if n == 0 {
break; }
total += n;
}
assert_eq!(total, 24 * 2048, "all 24 sectors should be drained");
});
}
#[test]
fn non_multiple_of_three_extent_errors_on_tail() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![Extent {
start_lba: 100,
sector_count: 8,
}];
let src = PatternSource { capacity: 200 };
let mut pf = PrefetchedSectorSource::new(src, extents, 3, None).expect("spawn");
let mut buf = vec![0u8; 3 * 2048];
let n0 = pf.read_sectors(0, 3, &mut buf, false).unwrap();
assert_eq!(n0, 3 * 2048);
let n1 = pf.read_sectors(0, 3, &mut buf, false).unwrap();
assert_eq!(n1, 3 * 2048);
let err = pf.read_sectors(0, 3, &mut buf, false);
assert!(
err.is_err(),
"non-unit-aligned tail must error, got Ok({:?})",
err
);
});
}
#[test]
fn short_read_does_not_desync_stream() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![Extent {
start_lba: 0,
sector_count: 9,
}];
let src = ShortFirstSource {
capacity: 9,
first: true,
};
let mut pf = PrefetchedSectorSource::new(src, extents, 9, None).expect("spawn");
let mut buf = vec![0u8; 9 * 2048];
let mut total = 0usize;
for _ in 0..16 {
let n = pf.read_sectors(0, 9, &mut buf, false).unwrap();
if n == 0 {
break;
}
total += n;
}
assert_eq!(
total,
9 * 2048,
"short read must not drop sectors; all 9 must be delivered"
);
});
}
use std::sync::{Arc, Mutex};
struct RecordingSource {
capacity: u32,
calls: Arc<Mutex<Vec<(u32, u16)>>>,
}
impl SectorSource for RecordingSource {
fn capacity_sectors(&self) -> u32 {
self.capacity
}
fn read_sectors(
&mut self,
lba: u32,
count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
self.calls.lock().unwrap().push((lba, count));
let bytes = count as usize * 2048;
buf[..bytes].fill((lba & 0xff) as u8);
Ok(bytes)
}
}
struct ErrorSource;
impl SectorSource for ErrorSource {
fn read_sectors(
&mut self,
_lba: u32,
_count: u16,
_buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
Err(crate::error::Error::IoError {
source: std::io::Error::from(std::io::ErrorKind::PermissionDenied),
})
}
}
struct PartialSectorSource;
impl SectorSource for PartialSectorSource {
fn read_sectors(
&mut self,
_lba: u32,
_count: u16,
buf: &mut [u8],
_recovery: bool,
) -> Result<usize> {
let n = 2048 + 100;
buf[..n].fill(0xab);
Ok(n)
}
}
fn drain_direct(
pf: &mut PrefetchedSectorSource,
buf_sectors: u16,
max_iters: usize,
) -> (Vec<u8>, Result<usize>) {
let mut buf = vec![0u8; buf_sectors as usize * 2048];
let mut out = Vec::new();
let mut last: Result<usize> = Ok(0);
for _ in 0..max_iters {
let r = pf.read_sectors(0, buf_sectors, &mut buf, false);
match r {
Ok(0) => {
last = Ok(0);
break;
}
Ok(n) => {
out.extend_from_slice(&buf[..n]);
last = Ok(n);
}
Err(e) => {
last = Err(e);
break;
}
}
}
(out, last)
}
#[test]
fn capacity_sectors_sums_all_extents() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![
Extent {
start_lba: 0,
sector_count: 9,
},
Extent {
start_lba: 100,
sector_count: 6,
},
Extent {
start_lba: 500,
sector_count: 3,
},
];
let src = PatternSource { capacity: 9999 };
let pf = PrefetchedSectorSource::new(src, extents, 3, None).expect("spawn");
assert_eq!(pf.capacity_sectors(), 18);
let (rx, recycle_tx, shell) = pf.into_channels();
drop(rx);
drop(recycle_tx);
drop(shell);
});
}
#[test]
fn capacity_sectors_clamps_on_overflow() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![
Extent {
start_lba: 0,
sector_count: u32::MAX,
},
Extent {
start_lba: 0,
sector_count: u32::MAX,
},
];
let pf =
PrefetchedSectorSource::new(EndlessZeroSource, extents, 3, None).expect("spawn");
assert_eq!(
pf.capacity_sectors(),
u32::MAX,
"summed total must saturate at u32::MAX, not wrap"
);
let (rx, recycle_tx, shell) = pf.into_channels();
drop(rx);
drop(recycle_tx);
drop(shell);
});
}
#[test]
fn producer_walks_extents_in_order_at_correct_lbas() {
with_watchdog(Duration::from_secs(10), || {
let calls = Arc::new(Mutex::new(Vec::new()));
let extents = vec![
Extent {
start_lba: 1000,
sector_count: 6, },
Extent {
start_lba: 50,
sector_count: 3, },
];
let src = RecordingSource {
capacity: 99999,
calls: calls.clone(),
};
let mut pf = PrefetchedSectorSource::new(src, extents, 3, None).expect("spawn");
let (got, last) = drain_direct(&mut pf, 3, 16);
assert_eq!(last.unwrap(), 0, "should reach EOF");
assert_eq!(got.len(), (6 + 3) * 2048);
drop(pf);
let recorded = calls.lock().unwrap().clone();
assert_eq!(
recorded,
vec![(1000, 3), (1003, 3), (50, 3)],
"extents must be walked in list order at their start_lba+offset"
);
});
}
#[test]
fn batch_trimmed_to_whole_units() {
with_watchdog(Duration::from_secs(10), || {
let calls = Arc::new(Mutex::new(Vec::new()));
let extents = vec![Extent {
start_lba: 0,
sector_count: 9,
}];
let src = RecordingSource {
capacity: 9,
calls: calls.clone(),
};
let mut pf = PrefetchedSectorSource::new(src, extents, 5, None).expect("spawn");
let (got, last) = drain_direct(&mut pf, 5, 16);
assert_eq!(last.unwrap(), 0);
assert_eq!(got.len(), 9 * 2048);
drop(pf);
let recorded = calls.lock().unwrap().clone();
assert!(
recorded.iter().all(|&(_, c)| c % SECTOR_ALIGNMENT == 0),
"every issued read must be a whole number of units, got {recorded:?}"
);
assert!(
recorded.iter().all(|&(_, c)| c == 3),
"batch=5 must trim to one 3-sector unit per read, got {recorded:?}"
);
});
}
#[test]
fn unit_aligned_extent_delivers_all_and_eofs() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![Extent {
start_lba: 7,
sector_count: 12,
}];
let src = PatternSource { capacity: 100 };
let mut pf = PrefetchedSectorSource::new(src, extents, 6, None).expect("spawn");
let (got, last) = drain_direct(&mut pf, 6, 16);
assert_eq!(
last.unwrap(),
0,
"unit-aligned extent must EOF cleanly, not error"
);
assert_eq!(got.len(), 12 * 2048);
});
}
#[test]
fn reader_error_propagates_with_kind() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![Extent {
start_lba: 0,
sector_count: 3,
}];
let mut pf = PrefetchedSectorSource::new(ErrorSource, extents, 3, None).expect("spawn");
let mut buf = vec![0u8; 3 * 2048];
let r = pf.read_sectors(0, 3, &mut buf, false);
let err = r.expect_err("reader error must surface as Err, not EOF");
let io: std::io::Error = err.into();
assert_eq!(
io.kind(),
std::io::ErrorKind::PermissionDenied,
"underlying ErrorKind must survive the channel round-trip"
);
});
}
#[test]
fn non_sector_multiple_read_rejected() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![Extent {
start_lba: 0,
sector_count: 9,
}];
let mut pf =
PrefetchedSectorSource::new(PartialSectorSource, extents, 3, None).expect("spawn");
let mut buf = vec![0u8; 3 * 2048];
let r = pf.read_sectors(0, 3, &mut buf, false);
let err = r.expect_err("split-sector read must be rejected");
let io: std::io::Error = err.into();
assert_eq!(
io.kind(),
std::io::ErrorKind::InvalidInput,
"split-sector read maps to ExtentNotUnitAligned (InvalidInput)"
);
});
}
#[test]
fn direct_read_too_small_buffer_errors() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![Extent {
start_lba: 0,
sector_count: 6,
}];
let src = PatternSource { capacity: 6 };
let mut pf = PrefetchedSectorSource::new(src, extents, 3, None).expect("spawn");
let mut tiny = vec![0u8; 2048];
let r = pf.read_sectors(0, 1, &mut tiny, false);
let err = r.expect_err("too-small buffer must error, not truncate");
let io: std::io::Error = err.into();
assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput);
drop(pf);
});
}
#[test]
fn delivered_bytes_match_source_exactly() {
with_watchdog(Duration::from_secs(10), || {
let start = 40u32;
let count = 9u32; let extents = vec![Extent {
start_lba: start,
sector_count: count,
}];
let src = PatternSource { capacity: 1000 };
let mut pf = PrefetchedSectorSource::new(src, extents, 3, None).expect("spawn");
let (got, last) = drain_direct(&mut pf, 3, 16);
assert_eq!(last.unwrap(), 0);
assert_eq!(got.len(), (count as usize) * 2048);
for i in 0..count {
let tag = ((start + i) & 0xff) as u8;
let off = i as usize * 2048;
assert!(
got[off..off + 2048].iter().all(|b| *b == tag),
"sector {i} (lba {}) content mismatch",
start + i
);
}
});
}
#[test]
fn empty_extents_eof_immediately() {
with_watchdog(Duration::from_secs(10), || {
let pf =
PrefetchedSectorSource::new(EndlessZeroSource, Vec::new(), 3, None).expect("spawn");
assert_eq!(pf.capacity_sectors(), 0);
let mut pf = pf;
let mut buf = vec![0u8; 3 * 2048];
let n = pf.read_sectors(0, 3, &mut buf, false).unwrap();
assert_eq!(n, 0, "empty extent list must EOF immediately");
});
}
#[test]
fn zero_length_extent_is_skipped() {
with_watchdog(Duration::from_secs(10), || {
let calls = Arc::new(Mutex::new(Vec::new()));
let extents = vec![
Extent {
start_lba: 10,
sector_count: 3,
},
Extent {
start_lba: 20,
sector_count: 0, },
Extent {
start_lba: 30,
sector_count: 3,
},
];
let src = RecordingSource {
capacity: 9999,
calls: calls.clone(),
};
let mut pf = PrefetchedSectorSource::new(src, extents, 3, None).expect("spawn");
let (got, last) = drain_direct(&mut pf, 3, 16);
assert_eq!(last.unwrap(), 0);
assert_eq!(got.len(), 6 * 2048, "two non-empty extents = 6 sectors");
drop(pf);
let recorded = calls.lock().unwrap().clone();
assert_eq!(
recorded,
vec![(10, 3), (30, 3)],
"empty extent must produce no read"
);
});
}
#[test]
fn four_sector_extent_errors_on_one_sector_tail() {
with_watchdog(Duration::from_secs(10), || {
let extents = vec![Extent {
start_lba: 0,
sector_count: 4,
}];
let src = PatternSource { capacity: 100 };
let mut pf = PrefetchedSectorSource::new(src, extents, 9, None).expect("spawn");
let mut buf = vec![0u8; 9 * 2048];
let n0 = pf.read_sectors(0, 9, &mut buf, false).unwrap();
assert_eq!(n0, 3 * 2048, "first batch must be exactly one unit");
let r = pf.read_sectors(0, 9, &mut buf, false);
let err = r.expect_err("1-sector tail must error");
let io: std::io::Error = err.into();
assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput);
});
}
#[test]
fn many_extents_drain_without_deadlock() {
with_watchdog(Duration::from_secs(15), || {
let extents: Vec<Extent> = (0..10)
.map(|i| Extent {
start_lba: i * 1000,
sector_count: 3,
})
.collect();
let src = PatternSource { capacity: 999999 };
let mut pf = PrefetchedSectorSource::new(src, extents, 3, None).expect("spawn");
let (got, last) = drain_direct(&mut pf, 3, 64);
assert_eq!(last.unwrap(), 0);
assert_eq!(got.len(), 30 * 2048, "all 10 extents must be drained");
});
}
}