use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use nix::libc;
use rstest::rstest;
use super::super::*;
use super::*;
use crate::common::generic_consts::Sequential;
#[rstest]
#[case(false)]
#[case(true)]
fn test_io_uring_file_for_u64(#[case] o_direct: bool) -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test_u64.bin");
let data: Vec<u64> = (0..128).collect();
let bytes = bytemuck::cast_slice(&data);
fs_err::write(&path, bytes).unwrap();
let opts = OpenOptions::new_for_test();
let extra = IoUringOpenExtra {
prevent_caching: o_direct,
};
let fs = IoUringFs::from_context(Default::default())?;
let file = TypedStorage::<IoUringFile, u64>::open(&fs, &path, opts, extra)?;
let read_back = file.read::<Sequential>(ReadRange {
byte_offset: 0,
length: data.len() as u64,
})?;
assert_eq!(read_back.as_ref(), &data);
let read_sub = file.read::<Sequential>(ReadRange {
byte_offset: 10 * size_of::<u64>() as u64,
length: 20,
})?;
assert_eq!(read_sub.as_ref(), &data[10..30]);
let len = file.len()?;
assert_eq!(len, 128);
Ok(())
}
#[rstest]
#[case(false)]
#[case(true)]
fn test_io_uring_read_batch(#[case] o_direct: bool) -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test_batch.bin");
let data: Vec<u64> = (0..256).collect();
fs_err::write(&path, bytemuck::cast_slice(&data)).unwrap();
let opts = OpenOptions::new_for_test();
let extra = IoUringOpenExtra {
prevent_caching: o_direct,
};
let fs = IoUringFs::from_context(Default::default())?;
let file = TypedStorage::<IoUringFile, u64>::open(&fs, &path, opts, extra)?;
let elem = size_of::<u64>() as u64;
#[rustfmt::skip]
let ranges = [
ReadRange { byte_offset: 0, length: 10 }, ReadRange { byte_offset: 50 * elem, length: 20 }, ReadRange { byte_offset: 100 * elem, length: 5 }, ReadRange { byte_offset: 200 * elem, length: 56 }, ];
let expected: Vec<&[u64]> = vec![
&data[0..10],
&data[50..70],
&data[100..105],
&data[200..256],
];
let mut batch_results: Vec<(usize, Vec<u64>)> = Vec::new();
file.read_batch::<Sequential, _>(ranges.iter().copied().enumerate(), |idx, slice| {
batch_results.push((idx, slice.to_vec()));
Ok(())
})?;
batch_results.sort_by_key(|(idx, _)| *idx);
for (idx, items) in &batch_results {
assert_eq!(
items.as_slice(),
expected[*idx],
"read_batch mismatch at index {idx}"
);
}
let mut iter_results: Vec<(usize, Vec<u64>)> = Vec::new();
for record in file.read_iter::<Sequential, _>(ranges.iter().copied().enumerate())? {
let (idx, cow) = record?;
iter_results.push((idx, cow.into_owned()));
}
iter_results.sort_by_key(|(idx, _)| *idx);
for (idx, items) in &iter_results {
assert_eq!(
items.as_slice(),
expected[*idx],
"read_iter mismatch at index {idx}"
);
}
let many_ranges = (0..64).map(|i| ReadRange {
byte_offset: i as u64 * elem,
length: 1,
});
let mut count = 0;
for record in file.read_iter::<Sequential, _>(many_ranges.enumerate())? {
let (idx, cow) = record?;
assert_eq!(
cow.as_ref(),
&[data[idx]],
"many-ranges mismatch at index {idx}"
);
count += 1;
}
assert_eq!(count, 64);
Ok(())
}
#[rstest]
#[case(true)]
#[case(false)]
fn test_io_uring_concurrent_read_iter(#[case] o_direct: bool) -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let elem = size_of::<u64>() as u64;
const NUM_ELEMENTS: u64 = 6400;
const NUM_RANGES: u64 = 64;
const CHUNK: u64 = NUM_ELEMENTS / NUM_RANGES;
let path_a = dir.path().join("a.bin");
let data_a: Vec<u64> = (0..NUM_ELEMENTS).collect();
fs_err::write(&path_a, bytemuck::cast_slice(&data_a)).unwrap();
let path_b = dir.path().join("b.bin");
let data_b: Vec<u64> = (1_000_000..1_000_000 + NUM_ELEMENTS).collect();
fs_err::write(&path_b, bytemuck::cast_slice(&data_b)).unwrap();
let opts = OpenOptions::new_for_test();
let extra = IoUringOpenExtra {
prevent_caching: o_direct,
};
let fs = IoUringFs::from_context(Default::default())?;
let file_a = TypedStorage::<IoUringFile, u64>::open(&fs, &path_a, opts, extra)?;
let file_b = TypedStorage::<IoUringFile, u64>::open(&fs, &path_b, opts, extra)?;
let ranges_a = (0..NUM_RANGES).map(|i| ReadRange {
byte_offset: i * CHUNK * elem,
length: CHUNK,
});
let ranges_b = (0..NUM_RANGES).map(|i| ReadRange {
byte_offset: i * CHUNK * elem,
length: CHUNK,
});
let iter_a = file_a.read_iter::<Sequential, _>(ranges_a.enumerate())?;
let iter_b = file_b.read_iter::<Sequential, _>(ranges_b.enumerate())?;
let mut count = 0u64;
for (rec_a, rec_b) in iter_a.zip(iter_b) {
let (idx_a, cow_a) = rec_a?;
let (idx_b, cow_b) = rec_b?;
let start_a = idx_a as u64 * CHUNK;
assert_eq!(
cow_a.as_ref(),
&data_a[start_a as usize..(start_a + CHUNK) as usize],
"file A mismatch at range index {idx_a}"
);
let start_b = idx_b as u64 * CHUNK;
assert_eq!(
cow_b.as_ref(),
&data_b[start_b as usize..(start_b + CHUNK) as usize],
"file B mismatch at range index {idx_b}"
);
count += 1;
}
assert_eq!(count, NUM_RANGES);
Ok(())
}
#[rstest]
#[case(false)]
#[case(true)]
fn test_io_uring_read_multi_iter_basic(#[case] o_direct: bool) -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let elem = size_of::<u64>() as u64;
let path_0 = dir.path().join("f0.bin");
let data_0: Vec<u64> = (0..128).collect();
fs_err::write(&path_0, bytemuck::cast_slice(&data_0)).unwrap();
let path_1 = dir.path().join("f1.bin");
let data_1: Vec<u64> = (1000..1128).collect();
fs_err::write(&path_1, bytemuck::cast_slice(&data_1)).unwrap();
let opts = OpenOptions::new_for_test();
let extra = IoUringOpenExtra {
prevent_caching: o_direct,
};
let fs = IoUringFs::from_context(Default::default())?;
let file_0 = fs.open(&path_0, opts, extra)?;
let file_1 = fs.open(&path_1, opts, extra)?;
let files = [file_0, file_1];
#[rustfmt::skip]
let reads = [
('a', &files[0], ReadRange { byte_offset: 0, length: 10 }), ('b', &files[1], ReadRange { byte_offset: 20 * elem, length: 5 }), ('c', &files[0], ReadRange { byte_offset: 50 * elem, length: 20 }), ('d', &files[1], ReadRange { byte_offset: 0, length: 10 }), ];
let expected = [
('a', data_0[0..10].to_vec()),
('b', data_1[20..25].to_vec()),
('c', data_0[50..70].to_vec()),
('d', data_1[0..10].to_vec()),
];
let mut results: Vec<(char, Vec<u64>)> = Vec::new();
for record in IoUringFile::read_multi_iter::<Sequential, u64, _>(reads)? {
let (idx, cow) = record?;
results.push((idx, cow.into_owned()));
}
results.sort_by_key(|(idx, _)| *idx);
for (result, expected) in std::iter::zip(&results, &expected) {
assert_eq!(result, expected, "mismatch for read index {}", result.0);
}
Ok(())
}
#[rstest]
#[case(false)]
#[case(true)]
fn test_io_uring_read_multi_iter_many_ranges(#[case] o_direct: bool) -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let elem = size_of::<u64>() as u64;
const NUM_FILES: usize = 4;
const ELEMENTS_PER_FILE: u64 = 256;
const RANGES_PER_FILE: u64 = 20;
let mut all_data: Vec<Vec<u64>> = Vec::new();
let mut files: Vec<IoUringFile> = Vec::new();
let opts = OpenOptions::new_for_test();
let extra = IoUringOpenExtra {
prevent_caching: o_direct,
};
let fs = IoUringFs::from_context(Default::default())?;
for i in 0..NUM_FILES {
let base = (i as u64) * 10_000;
let data: Vec<u64> = (base..base + ELEMENTS_PER_FILE).collect();
let path = dir.path().join(format!("f{i}.bin"));
fs_err::write(&path, bytemuck::cast_slice(&data)).unwrap();
let file = fs.open(&path, opts, extra)?;
files.push(file);
all_data.push(data);
}
let reads: Vec<((usize, usize), &IoUringFile, ReadRange)> = (0..NUM_FILES as u64
* RANGES_PER_FILE)
.map(|i| {
let file_idx = (i as usize) % NUM_FILES;
let range_idx = i / NUM_FILES as u64;
let offset = range_idx * 10; (
(file_idx, offset as usize),
&files[file_idx],
ReadRange {
byte_offset: offset * elem,
length: 10,
},
)
})
.collect();
let mut results: Vec<((usize, usize), Vec<u64>)> = Vec::new();
for record in IoUringFile::read_multi_iter::<Sequential, u64, _>(reads)? {
let (idx, cow) = record?;
results.push((idx, cow.into_owned()));
}
assert_eq!(results.len(), NUM_FILES * RANGES_PER_FILE as usize);
results.sort_by_key(|(idx, _)| *idx);
for ((file_idx, offset), result) in results {
assert_eq!(
result.as_slice(),
&all_data[file_idx][offset..offset + 10],
"data mismatch at offset {offset}, file {file_idx}"
);
}
Ok(())
}
#[test]
fn test_io_uring_read_multi_callback_matches_iter() -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let elem = size_of::<u64>() as u64;
let path_a = dir.path().join("a.bin");
let data_a: Vec<u64> = (0..200).collect();
fs_err::write(&path_a, bytemuck::cast_slice(&data_a)).unwrap();
let path_b = dir.path().join("b.bin");
let data_b: Vec<u64> = (5000..5200).collect();
fs_err::write(&path_b, bytemuck::cast_slice(&data_b)).unwrap();
let opts = OpenOptions::new_for_test();
#[allow(clippy::default_constructed_unit_structs)]
let fs = IoUringFs::default();
let file_a = fs.open(&path_a, opts, IoUringOpenExtra::default())?;
let file_b = fs.open(&path_b, opts, IoUringOpenExtra::default())?;
let files = [file_a, file_b];
#[rustfmt::skip]
let reads: Vec<(usize, &IoUringFile, ReadRange)> = vec![
(0, &files[0], ReadRange { byte_offset: 0, length: 50 }),
(1, &files[1], ReadRange { byte_offset: 10 * elem, length: 30 }),
(2, &files[0], ReadRange { byte_offset: 100 * elem, length: 50 }),
(3, &files[1], ReadRange { byte_offset: 0, length: 100 }),
(4, &files[0], ReadRange { byte_offset: 150 * elem, length: 50 }),
];
let mut callback_results: Vec<(usize, Vec<u64>)> = Vec::new();
IoUringFile::read_multi::<Sequential, u64, _>(reads.clone(), |idx, data| {
callback_results.push((idx, data.to_vec()));
Ok(())
})?;
let mut iter_results: Vec<(usize, Vec<u64>)> = Vec::new();
for record in IoUringFile::read_multi_iter::<Sequential, u64, _>(reads)? {
let (idx, cow) = record?;
iter_results.push((idx, cow.into_owned()));
}
callback_results.sort_by_key(|(idx, _)| *idx);
iter_results.sort_by_key(|(idx, _)| *idx);
assert_eq!(callback_results.len(), iter_results.len());
for (cb, it) in callback_results.iter().zip(iter_results.iter()) {
assert_eq!(cb.0, it.0, "operation index mismatch");
assert_eq!(cb.1, it.1, "data mismatch at op {}", cb.0);
}
Ok(())
}
extern "C" fn noop_signal_handler(_sig: libc::c_int) {}
#[test]
fn test_io_uring_eintr_handling() -> Result<()> {
unsafe {
let mut sa: libc::sigaction = std::mem::zeroed();
sa.sa_sigaction = noop_signal_handler as *const () as usize;
sa.sa_flags = 0;
libc::sigemptyset(&mut sa.sa_mask);
let ret = libc::sigaction(libc::SIGUSR1, &sa, std::ptr::null_mut());
assert_eq!(ret, 0, "failed to install SIGUSR1 handler");
}
const NUM_ELEMENTS: u64 = 4 * 1024 * 1024;
const RANGES_PER_ROUND: u64 = 128;
const TEST_DURATION_SECS: u64 = 10;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("eintr_test.bin");
let data: Vec<u64> = (0..NUM_ELEMENTS).collect();
fs_err::write(&path, bytemuck::cast_slice(&data)).unwrap();
#[allow(clippy::default_constructed_unit_structs)]
let fs = IoUringFs::default();
let file = TypedStorage::<IoUringFile, u64>::open(
&fs,
&path,
OpenOptions::new_for_test(),
IoUringOpenExtra::default(),
)?;
let stop = Arc::new(AtomicBool::new(false));
let signals_sent = Arc::new(AtomicU64::new(0));
let target_thread: libc::pthread_t = unsafe { libc::pthread_self() };
let signal_thread = {
let stop = stop.clone();
let sent = signals_sent.clone();
std::thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
unsafe { libc::pthread_kill(target_thread, libc::SIGUSR1) };
sent.fetch_add(1, Ordering::Relaxed);
std::thread::sleep(std::time::Duration::from_micros(50));
}
})
};
let elem = size_of::<u64>() as u64;
let chunk_size = NUM_ELEMENTS / RANGES_PER_ROUND;
let mut eintr_errors = 0u64;
let mut panics = 0u64;
let mut rounds = 0u64;
let start = std::time::Instant::now();
while start.elapsed().as_secs() < TEST_DURATION_SECS {
rounds += 1;
file.clear_ram_cache().ok();
let ranges: Vec<(u64, ReadRange)> = (0..RANGES_PER_ROUND)
.map(|i| {
(
i,
ReadRange {
byte_offset: i * chunk_size * elem,
length: chunk_size,
},
)
})
.collect();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut errors = 0u64;
let Ok(iter) = file.read_iter::<Sequential, _>(ranges) else {
return 1;
};
for record in iter {
if record.is_err() {
errors += 1;
break;
}
}
errors
}));
match result {
Ok(n) => eintr_errors += n,
Err(_) => panics += 1,
}
if eintr_errors > 0 || panics > 0 {
break;
}
}
stop.store(true, Ordering::Relaxed);
signal_thread.join().unwrap();
let total_signals = signals_sent.load(Ordering::Relaxed);
let failures = eintr_errors + panics;
assert_eq!(
failures, 0,
"io_uring submit_and_wait does not retry on EINTR: \
{eintr_errors} errors, {panics} panics in {rounds} rounds ({total_signals} signals). \
Fix: retry submit_and_wait when it returns io::ErrorKind::Interrupted."
);
Ok(())
}