use std::path::{Path, PathBuf};
#[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_BUF_SIZE: usize = 1024 * 1024;
#[cfg(target_os = "linux")]
pub struct UringWriter {
ring: io_uring::IoUring,
file: std::fs::File,
path: PathBuf,
staging: Vec<u8>,
offset: u64,
}
#[cfg(not(target_os = "linux"))]
pub struct UringWriter;
#[cfg(target_os = "linux")]
impl UringWriter {
pub fn create(path: &Path, buf_size: usize) -> Option<Self> {
let ring = io_uring::IoUring::new(QUEUE_DEPTH).ok()?;
let file = std::fs::File::create(path).ok()?;
Some(Self {
ring,
file,
path: path.to_path_buf(),
staging: Vec::with_capacity(buf_size.max(1)),
offset: 0,
})
}
pub fn new(path: &Path) -> Option<Self> {
Self::create(path, DEFAULT_BUF_SIZE)
}
pub fn append(&mut self, data: &[u8]) -> crate::Result<()> {
if data.is_empty() {
return Ok(());
}
self.staging.clear();
self.staging.extend_from_slice(data);
let mut written: usize = 0;
let total = self.staging.len();
while written < total {
let remaining = total - written;
let chunk = remaining.min(u32::MAX as usize) as u32;
let buf_ptr = unsafe { self.staging.as_ptr().add(written) };
let write_op = io_uring::opcode::Write::new(
io_uring::types::Fd(self.file.as_raw_fd()),
buf_ptr,
chunk,
)
.offset(self.offset)
.build()
.user_data(0);
unsafe {
self.ring
.submission()
.push(&write_op)
.map_err(|e| crate::Error::Storage {
engine: "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: "spill".into(),
detail: "uring write 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 {
return Err(crate::Error::Io(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"uring write made no progress (0 bytes) with data remaining",
)));
}
written += n;
self.offset += n as u64;
}
Ok(())
}
pub fn flush(&mut self) -> crate::Result<()> {
let fsync_op = io_uring::opcode::Fsync::new(io_uring::types::Fd(self.file.as_raw_fd()))
.build()
.user_data(0);
unsafe {
self.ring
.submission()
.push(&fsync_op)
.map_err(|e| crate::Error::Storage {
engine: "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: "spill".into(),
detail: "uring fsync 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)));
}
Ok(())
}
pub fn finish(mut self) -> crate::Result<PathBuf> {
self.flush()?;
Ok(self.path)
}
}
#[cfg(not(target_os = "linux"))]
impl UringWriter {
pub fn create(_path: &Path, _buf_size: usize) -> Option<Self> {
None
}
pub fn new(_path: &Path) -> Option<Self> {
None
}
pub fn append(&mut self, _data: &[u8]) -> crate::Result<()> {
Ok(())
}
pub fn flush(&mut self) -> crate::Result<()> {
Ok(())
}
pub fn finish(self) -> crate::Result<PathBuf> {
Ok(PathBuf::new())
}
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
use crate::data::io::uring_reader::UringReader;
fn data_of(size: usize, seed: u8) -> Vec<u8> {
(0..size)
.map(|i| ((i + seed as usize) % 256) as u8)
.collect()
}
#[test]
fn round_trip_varied_chunks() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("spill.tmp");
let small = data_of(1024, 1);
let empty: Vec<u8> = Vec::new();
let large = data_of(3 * 1024 * 1024, 7); let tail = data_of(777, 200);
let mut expected = Vec::new();
{
let mut w = UringWriter::create(&path, 64 * 1024).unwrap();
w.append(&small).unwrap();
expected.extend_from_slice(&small);
w.append(&empty).unwrap();
expected.extend_from_slice(&empty);
w.append(&large).unwrap();
expected.extend_from_slice(&large);
w.append(&tail).unwrap();
expected.extend_from_slice(&tail);
let returned = w.finish().unwrap();
assert_eq!(returned, path);
}
let mut reader = UringReader::with_config(8, 4, 8 * 1024 * 1024).unwrap();
let results = reader.read_files(&[path.as_path()]);
assert_eq!(results.len(), 1);
assert_eq!(
results[0].len(),
expected.len(),
"spill file length mismatch"
);
assert_eq!(results[0], expected, "spill file contents mismatch");
}
#[test]
fn round_trip_matches_std_read() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("spill2.tmp");
let a = data_of(500, 0);
let b = data_of(50_000, 3);
let mut w = UringWriter::create(&path, 4096).unwrap();
w.append(&a).unwrap();
w.append(&b).unwrap();
w.finish().unwrap();
let mut expected = a.clone();
expected.extend_from_slice(&b);
let got = std::fs::read(&path).unwrap();
assert_eq!(got, expected);
}
#[test]
fn sequential_appends_ordered() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("ordered.tmp");
let mut expected = Vec::new();
let mut w = UringWriter::create(&path, 256).unwrap();
for seed in 0..16u8 {
let chunk = data_of(300, seed);
w.append(&chunk).unwrap();
expected.extend_from_slice(&chunk);
}
w.finish().unwrap();
let got = std::fs::read(&path).unwrap();
assert_eq!(got, expected, "appends must concatenate in order");
}
#[test]
fn flush_does_not_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("flush.tmp");
let mut w = UringWriter::create(&path, 4096).unwrap();
w.append(&data_of(2048, 9)).unwrap();
w.flush().unwrap();
w.flush().unwrap();
w.finish().unwrap();
}
#[test]
fn empty_file_round_trip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("empty.tmp");
let w = UringWriter::create(&path, 4096).unwrap();
let returned = w.finish().unwrap();
assert_eq!(returned, path);
let got = std::fs::read(&path).unwrap();
assert!(got.is_empty());
}
}