use std::path::Path;
#[cfg(target_os = "linux")]
use std::os::unix::io::AsRawFd;
#[cfg(target_os = "linux")]
const QUEUE_DEPTH: u32 = 8;
#[cfg(target_os = "linux")]
const DEFAULT_CHUNK_SIZE: usize = 256 * 1024;
#[cfg(target_os = "linux")]
pub struct UringSeqReader {
ring: io_uring::IoUring,
file: std::fs::File,
file_offset: u64,
buf: Vec<u8>,
pos: usize,
filled: usize,
eof: bool,
}
#[cfg(not(target_os = "linux"))]
pub struct UringSeqReader;
#[cfg(target_os = "linux")]
impl UringSeqReader {
pub fn open(path: &Path, chunk_size: usize) -> Option<Self> {
let ring = io_uring::IoUring::new(QUEUE_DEPTH).ok()?;
let file = std::fs::File::open(path).ok()?;
Some(Self {
ring,
file,
file_offset: 0,
buf: vec![0u8; chunk_size.max(1)],
pos: 0,
filled: 0,
eof: false,
})
}
pub fn open_default(path: &Path) -> Option<Self> {
Self::open(path, DEFAULT_CHUNK_SIZE)
}
fn refill(&mut self) -> crate::Result<()> {
self.pos = 0;
self.filled = 0;
let read_len = self.buf.len().min(u32::MAX as usize) as u32;
let buf_ptr = self.buf.as_mut_ptr();
let read_op = io_uring::opcode::Read::new(
io_uring::types::Fd(self.file.as_raw_fd()),
buf_ptr,
read_len,
)
.offset(self.file_offset)
.build()
.user_data(0);
unsafe {
self.ring
.submission()
.push(&read_op)
.map_err(|e| crate::Error::Storage {
engine: "sort_spill".into(),
detail: format!("uring submission queue push failed: {e}"),
})?;
}
self.ring.submit_and_wait(1).map_err(crate::Error::Io)?;
let cqe = self
.ring
.completion()
.next()
.ok_or_else(|| crate::Error::Storage {
engine: "sort_spill".into(),
detail: "uring read completion missing after submit_and_wait".into(),
})?;
let res = cqe.result();
if res < 0 {
return Err(crate::Error::Io(std::io::Error::from_raw_os_error(-res)));
}
let n = res as usize;
if n == 0 {
self.eof = true;
} else {
self.filled = n;
self.file_offset += n as u64;
}
Ok(())
}
pub fn read_exact(&mut self, dst: &mut [u8]) -> crate::Result<bool> {
let mut written = 0;
while written < dst.len() {
if self.pos == self.filled {
if self.eof {
return Ok(false);
}
self.refill()?;
if self.pos == self.filled {
return Ok(false);
}
}
let take = (dst.len() - written).min(self.filled - self.pos);
dst[written..written + take].copy_from_slice(&self.buf[self.pos..self.pos + take]);
self.pos += take;
written += take;
}
Ok(true)
}
}
#[cfg(not(target_os = "linux"))]
impl UringSeqReader {
pub fn open(_path: &Path, _chunk_size: usize) -> Option<Self> {
None
}
pub fn open_default(_path: &Path) -> Option<Self> {
None
}
pub fn read_exact(&mut self, _dst: &mut [u8]) -> crate::Result<bool> {
Ok(false)
}
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
fn data_of(size: usize, seed: u8) -> Vec<u8> {
(0..size)
.map(|i| ((i + seed as usize) % 256) as u8)
.collect()
}
#[test]
fn multiple_refills_varying_reads() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("multi.bin");
let original = data_of(5000, 11);
std::fs::write(&path, &original).unwrap();
let mut reader = UringSeqReader::open(&path, 64).unwrap();
let mut reconstructed = Vec::new();
for size in [1usize, 7, 64, 100, 333, 9, 256].iter().cycle() {
let remaining = original.len() - reconstructed.len();
if remaining == 0 {
break;
}
let want = (*size).min(remaining);
let mut dst = vec![0u8; want];
let ok = reader.read_exact(&mut dst).unwrap();
assert!(ok, "read_exact must succeed while bytes remain");
reconstructed.extend_from_slice(&dst);
}
assert_eq!(reconstructed, original);
}
#[test]
fn single_read_spanning_many_refills() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("span.bin");
let original = data_of(1000, 3);
std::fs::write(&path, &original).unwrap();
let mut reader = UringSeqReader::open(&path, 16).unwrap();
let mut dst = vec![0u8; 1000];
let ok = reader.read_exact(&mut dst).unwrap();
assert!(ok);
assert_eq!(dst, original);
}
#[test]
fn partial_at_eof_returns_false() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("partial.bin");
std::fs::write(&path, data_of(10, 1)).unwrap();
let mut reader = UringSeqReader::open(&path, 256).unwrap();
let mut dst = vec![0u8; 20];
let ok = reader.read_exact(&mut dst).unwrap();
assert!(!ok, "partial read at EOF must return Ok(false)");
}
#[test]
fn empty_file_returns_false() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("empty.bin");
std::fs::write(&path, b"").unwrap();
let mut reader = UringSeqReader::open_default(&path).unwrap();
let mut dst = [0u8; 1];
assert!(!reader.read_exact(&mut dst).unwrap());
}
#[test]
fn read_to_eof_then_more_returns_false() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("exact.bin");
let original = data_of(48, 5);
std::fs::write(&path, &original).unwrap();
let mut reader = UringSeqReader::open(&path, 16).unwrap();
let mut dst = vec![0u8; 48];
assert!(reader.read_exact(&mut dst).unwrap());
assert_eq!(dst, original);
let mut more = [0u8; 1];
assert!(!reader.read_exact(&mut more).unwrap());
}
#[test]
fn zero_len_read_is_true() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("z.bin");
std::fs::write(&path, data_of(4, 0)).unwrap();
let mut reader = UringSeqReader::open_default(&path).unwrap();
let mut dst: [u8; 0] = [];
assert!(reader.read_exact(&mut dst).unwrap());
}
}