use super::{
BlockRead, Fs, FsDirEntry, FsFile, FsMetadata, FsOpenOptions, QueuedRead, ReadDone, ReadQueue,
};
use crate::HashMap;
use core::sync::atomic::AtomicU64;
use io_uring::{IoUring, opcode, types};
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
const DEFAULT_SQ_ENTRIES: u32 = 256;
#[must_use]
pub fn is_io_uring_available() -> bool {
IoUring::new(2).is_ok()
}
pub struct IoUringFs {
inner: Arc<RingThread>,
}
impl IoUringFs {
pub fn new() -> io::Result<Self> {
Self::with_ring_size(DEFAULT_SQ_ENTRIES)
}
pub fn with_ring_size(sq_entries: u32) -> io::Result<Self> {
let inner = RingThread::spawn(sq_entries)?;
Ok(Self {
inner: Arc::new(inner),
})
}
fn read_blocks(
&self,
reqs: &mut [BlockRead<'_>],
mut on_read: Option<OnRead<'_>>,
) -> crate::io::Result<()> {
if let [req] = reqs
&& let Some(fd) = req.file.backing_fd()
{
return read_lone(fd, req, on_read);
}
if reqs.iter().all(|r| r.file.backing_fd().is_some()) {
return self
.inner
.submit_reads_multi(reqs, on_read)
.map_err(|(_, e)| crate::io::Error::from(e));
}
let (mut submittable, mut serial): (IndexedReads<'_, '_>, IndexedReads<'_, '_>) = reqs
.iter_mut()
.enumerate()
.partition(|(_, r)| r.file.backing_fd().is_some());
log::debug!(
"io_uring batched read: {} of {} requests lack a descriptor and are read serially",
serial.len(),
submittable.len() + serial.len(),
);
let ring_failure = if submittable.is_empty() {
None
} else {
let caller_index: Vec<usize> = submittable.iter().map(|(i, _)| *i).collect();
let mut group: Vec<&mut BlockRead<'_>> =
submittable.iter_mut().map(|(_, r)| &mut **r).collect();
let mut in_caller_order = on_read.as_deref_mut().map(|on_read| {
any_request(move |position, req| {
if let Some(&index) = caller_index.get(position) {
on_read(index, req);
}
})
});
self.inner
.submit_reads_multi(
&mut group,
in_caller_order
.as_mut()
.map(|f| f as &mut dyn FnMut(usize, &BlockRead<'_>)),
)
.err()
.map(|(at, e)| {
let caller_index = submittable.get(at).map_or(usize::MAX, |(i, _)| *i);
(caller_index, crate::io::Error::from(e))
})
};
let mut serial_failure: Option<(usize, crate::io::Error)> = None;
for (idx, req) in &mut serial {
let Some(offset) = req.offset.checked_add(req.buf.filled() as u64) else {
serial_failure = Some((
*idx,
crate::io::Error::from(io::Error::new(
io::ErrorKind::InvalidInput,
"read_blocks_batched: request offset overflows",
)),
));
break;
};
let dst = req.buf.unfilled_mut();
let want = dst.len();
match req.file.read_at(dst, offset) {
Ok(n) => {
req.buf.advance(n);
if n != want {
serial_failure = Some((
*idx,
crate::io::Error::from(io::Error::new(
io::ErrorKind::UnexpectedEof,
"read_blocks_batched: short read on a fixed-size block",
)),
));
break;
}
if let Some(on_read) = on_read.as_deref_mut() {
on_read(*idx, req);
}
}
Err(e) => {
serial_failure = Some((*idx, e));
break;
}
}
}
match (ring_failure, serial_failure) {
(Some((ring_at, ring_err)), Some((serial_at, serial_err))) => {
if serial_at < ring_at {
Err(serial_err)
} else {
Err(ring_err)
}
}
(Some((_, e)), None) | (None, Some((_, e))) => Err(e),
(None, None) => Ok(()),
}
}
}
impl Clone for IoUringFs {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl std::fmt::Debug for IoUringFs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IoUringFs").finish_non_exhaustive()
}
}
type OnRead<'f> = &'f mut dyn for<'a, 'b> FnMut(usize, &'a BlockRead<'b>);
fn read_lone(
fd: i32,
req: &mut BlockRead<'_>,
on_read: Option<OnRead<'_>>,
) -> crate::io::Result<()> {
use std::os::unix::fs::FileExt as _;
use std::os::unix::io::FromRawFd as _;
let Some(offset) = req.offset.checked_add(req.buf.filled() as u64) else {
return Err(crate::io::Error::from(io::Error::new(
io::ErrorKind::InvalidInput,
"read_blocks_batched: request offset overflows",
)));
};
#[expect(unsafe_code, reason = "see safety")]
let file = core::mem::ManuallyDrop::new(unsafe { File::from_raw_fd(fd) });
let dst = req.buf.unfilled_mut();
let want = dst.len();
file.read_exact_at(dst, offset)?;
req.buf.advance(want);
if let Some(on_read) = on_read {
on_read(0, req);
}
Ok(())
}
fn any_request<F: FnMut(usize, &BlockRead<'_>)>(f: F) -> F {
f
}
type IndexedReads<'a, 'b> = Vec<(usize, &'a mut BlockRead<'b>)>;
impl Fs for IoUringFs {
fn open(&self, path: &Path, opts: &FsOpenOptions) -> crate::io::Result<Box<dyn FsFile>> {
let mut builder = OpenOptions::new();
builder
.read(opts.read)
.write(opts.write)
.create(opts.create)
.create_new(opts.create_new)
.truncate(opts.truncate)
.append(opts.append);
#[cfg(feature = "std")]
super::open_flags::apply_open_flags(&mut builder, opts);
let file = builder.open(path)?;
#[cfg(feature = "std")]
super::open_flags::finish_open(&file, opts)?;
let cursor = if opts.append {
file.metadata()?.len()
} else {
0
};
Ok(Box::new(IoUringFile {
file,
cursor: AtomicU64::new(cursor),
is_append: opts.append,
ring: Arc::clone(&self.inner),
}))
}
fn read_blocks_batched(&self, reqs: &mut [BlockRead<'_>]) -> crate::io::Result<()> {
self.read_blocks(reqs, None)
}
fn read_blocks_batched_each(
&self,
reqs: &mut [BlockRead<'_>],
on_read: &mut dyn FnMut(usize, &BlockRead<'_>),
) -> crate::io::Result<()> {
self.read_blocks(reqs, Some(on_read))
}
fn read_queue(&self) -> Box<dyn ReadQueue + '_> {
Box::new(UringReadQueue::new(&self.inner))
}
fn reads_by_descriptor(&self) -> bool {
true
}
fn check_descriptor_batch(&self, reqs: &[BlockRead<'_>]) -> crate::io::Result<()> {
for req in reqs {
if req.buf.capacity() == 0 || req.file.backing_fd().is_none() {
continue;
}
if i32::try_from(req.buf.capacity() - req.buf.filled()).is_err() {
return Err(crate::io::Error::from(io::Error::new(
io::ErrorKind::InvalidInput,
"buffer exceeds i32::MAX",
)));
}
if req.offset.checked_add(req.buf.filled() as u64).is_none() {
return Err(crate::io::Error::from(io::Error::new(
io::ErrorKind::InvalidInput,
"submit_reads_multi: request offset overflows",
)));
}
}
Ok(())
}
fn create_dir_all(&self, path: &Path) -> crate::io::Result<()> {
std::fs::create_dir_all(path).map_err(crate::io::Error::from)
}
fn create_dir(&self, path: &Path) -> crate::io::Result<()> {
std::fs::create_dir(path).map_err(crate::io::Error::from)
}
fn read_dir(&self, path: &Path) -> crate::io::Result<Vec<FsDirEntry>> {
std::fs::read_dir(path)?
.map(|res| {
let entry = res?;
let file_type = entry.file_type()?;
let file_name_os = entry.file_name();
let file_name = file_name_os.into_string().map_err(|os| {
#[expect(
clippy::unnecessary_debug_formatting,
reason = "OsString has no Display impl — Debug is required"
)]
let msg = format!("non-UTF-8 filename in directory {}: {os:?}", path.display());
io::Error::new(io::ErrorKind::InvalidData, msg)
})?;
Ok(FsDirEntry {
path: entry.path(),
file_name,
is_dir: file_type.is_dir(),
})
})
.collect::<io::Result<Vec<_>>>()
.map_err(crate::io::Error::from)
}
fn remove_file(&self, path: &Path) -> crate::io::Result<()> {
std::fs::remove_file(path).map_err(crate::io::Error::from)
}
fn remove_dir_all(&self, path: &Path) -> crate::io::Result<()> {
std::fs::remove_dir_all(path).map_err(crate::io::Error::from)
}
fn remove_dir(&self, path: &Path) -> crate::io::Result<()> {
std::fs::remove_dir(path).map_err(crate::io::Error::from)
}
fn same_file(&self, a: &Path, b: &Path) -> crate::io::Result<bool> {
use std::os::unix::fs::MetadataExt;
let ma = std::fs::metadata(a).map_err(crate::io::Error::from)?;
let mb = std::fs::metadata(b).map_err(crate::io::Error::from)?;
Ok(ma.dev() == mb.dev() && ma.ino() == mb.ino())
}
fn read_link(&self, path: &Path) -> crate::io::Result<Option<crate::path::PathBuf>> {
super::StdFs.read_link(path)
}
fn symlink(&self, target: &Path, link: &Path, to_dir: bool) -> crate::io::Result<()> {
super::StdFs.symlink(target, link, to_dir)
}
fn symlink_to_dir(&self, path: &Path) -> crate::io::Result<bool> {
super::StdFs.symlink_to_dir(path)
}
fn rename(&self, from: &Path, to: &Path) -> crate::io::Result<()> {
std::fs::rename(from, to).map_err(crate::io::Error::from)
}
fn metadata(&self, path: &Path) -> crate::io::Result<FsMetadata> {
let m = std::fs::metadata(path)?;
Ok(FsMetadata {
len: m.len(),
is_dir: m.is_dir(),
is_file: m.is_file(),
})
}
fn available_space(&self, path: &Path) -> crate::io::Result<u64> {
super::statvfs_available_space(path).map_err(crate::io::Error::from)
}
fn allocated_size(&self, path: &Path) -> crate::io::Result<Option<u64>> {
Ok(super::unix_allocated_size(path)?)
}
fn extent_is_hole(
&self,
path: &Path,
offset: u64,
len: u64,
) -> crate::io::Result<Option<bool>> {
Fs::extent_is_hole(&super::StdFs, path, offset, len)
}
fn extent_contains_hole(
&self,
path: &Path,
offset: u64,
len: u64,
) -> crate::io::Result<Option<bool>> {
Fs::extent_contains_hole(&super::StdFs, path, offset, len)
}
fn sync_directory(&self, path: &Path) -> crate::io::Result<()> {
let dir = File::open(path)?;
if !dir.metadata()?.is_dir() {
return Err(crate::io::Error::new(
crate::io::ErrorKind::InvalidInput,
"sync_directory: path is not a directory",
));
}
dir.sync_all().map_err(crate::io::Error::from)
}
fn exists(&self, path: &Path) -> crate::io::Result<bool> {
path.try_exists().map_err(crate::io::Error::from)
}
fn hard_link(&self, src: &Path, dst: &Path) -> crate::io::Result<()> {
super::StdFs.hard_link(src, dst)
}
fn backend_id(&self) -> Option<u64> {
super::StdFs.backend_id()
}
fn volume_id(&self, path: &Path) -> Option<u64> {
super::StdFs.volume_id(path)
}
fn capabilities(&self, path: &Path) -> super::FsCapabilities {
super::StdFs.capabilities(path)
}
fn try_disable_cow(&self, path: &Path) -> crate::io::Result<()> {
super::StdFs.try_disable_cow(path)
}
fn punch_hole(&self, path: &Path, offset: u64, len: u64) -> crate::io::Result<()> {
super::StdFs.punch_hole(path, offset, len)
}
fn reflink_file(&self, src: &Path, dst: &Path) -> crate::io::Result<()> {
super::StdFs.reflink_file(src, dst)
}
fn hard_link_count(&self, path: &Path) -> crate::io::Result<u64> {
super::StdFs.hard_link_count(path)
}
}
pub(crate) struct IoUringFile {
file: File,
cursor: AtomicU64,
is_append: bool,
ring: Arc<RingThread>,
}
impl FsFile for IoUringFile {
fn sync_all(&self) -> crate::io::Result<()> {
FsFile::sync_all(&self.file)
}
fn sync_data(&self) -> crate::io::Result<()> {
FsFile::sync_data(&self.file)
}
fn metadata(&self) -> crate::io::Result<FsMetadata> {
let m = self.file.metadata()?;
Ok(FsMetadata {
len: m.len(),
is_dir: m.is_dir(),
is_file: m.is_file(),
})
}
fn hard_link_count(&self) -> crate::io::Result<u64> {
use std::os::unix::fs::MetadataExt as _;
Ok(self.file.metadata()?.nlink())
}
fn set_len(&self, size: u64) -> crate::io::Result<()> {
self.file.set_len(size).map_err(crate::io::Error::from)
}
fn read_at(&self, buf: &mut [u8], offset: u64) -> crate::io::Result<usize> {
FsFile::read_at(&self.file, buf, offset)
}
fn read_many(&self, regions: &mut [(u64, &mut [u8])]) -> crate::io::Result<()> {
if regions.len() < 2 {
return FsFile::read_many(&self.file, regions);
}
self.ring
.submit_reads(self.file.as_raw_fd(), regions)
.map_err(crate::io::Error::from)
}
fn read_at_buf(&self, buf: &mut super::ReadBuf<'_>, offset: u64) -> crate::io::Result<()> {
#[expect(unsafe_code, reason = "see safety")]
unsafe {
buf.fill_with(|bytes| FsFile::read_at(self, bytes, offset))
}
}
fn read_many_buf(&self, regions: &mut [(u64, super::ReadBuf<'_>)]) -> crate::io::Result<()> {
if regions.len() < 2 {
return FsFile::read_many_buf(&self.file, regions);
}
self.ring
.submit_reads(self.file.as_raw_fd(), regions)
.map_err(crate::io::Error::from)?;
#[expect(unsafe_code, reason = "see safety")]
unsafe {
super::read_buf::assume_regions_filled(regions);
}
Ok(())
}
fn backing_fd(&self) -> Option<i32> {
Some(self.file.as_raw_fd())
}
fn start_writeback(&self, offset: u64, len: u64) -> crate::io::Result<()> {
FsFile::start_writeback(&self.file, offset, len)
}
fn set_write_lifetime(&self, lifetime: super::WriteLifetime) -> crate::io::Result<()> {
FsFile::set_write_lifetime(&self.file, lifetime)
}
fn hint(&self, hint: super::FileHint) -> crate::io::Result<()> {
FsFile::hint(&self.file, hint)
}
fn lock_exclusive(&self) -> crate::io::Result<()> {
FsFile::lock_exclusive(&self.file)
}
fn try_lock_exclusive(&self) -> crate::io::Result<bool> {
FsFile::try_lock_exclusive(&self.file)
}
}
impl Read for IoUringFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let cursor = self.cursor.get_mut();
let n = std::os::unix::fs::FileExt::read_at(&self.file, buf, *cursor)?;
*cursor += n as u64;
Ok(n)
}
}
impl Write for IoUringFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let cursor = self.cursor.get_mut();
if self.is_append {
*cursor = self.file.metadata()?.len();
}
let n = std::os::unix::fs::FileExt::write_at(&self.file, buf, *cursor)?;
*cursor += n as u64;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Seek for IoUringFile {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let cursor = self.cursor.get_mut();
let new_pos = match pos {
SeekFrom::Start(n) => n,
SeekFrom::Current(n) => if n >= 0 {
cursor.checked_add(n.unsigned_abs())
} else {
cursor.checked_sub(n.unsigned_abs())
}
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "seek position out of range")
})?,
SeekFrom::End(n) => {
let len = self.file.metadata()?.len();
if n >= 0 {
len.checked_add(n.unsigned_abs())
} else {
len.checked_sub(n.unsigned_abs())
}
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "seek position out of range")
})?
}
};
*cursor = new_pos;
Ok(new_pos)
}
}
struct UnsafeSendMutPtr(*mut u8);
#[expect(unsafe_code, reason = "marking raw-pointer wrapper as Send")]
unsafe impl Send for UnsafeSendMutPtr {}
struct BatchRead {
fd: i32,
buf: UnsafeSendMutPtr,
len: u32,
offset: u64,
}
enum Submission {
Batch {
reads: Vec<BatchRead>,
done: mpsc::SyncSender<(usize, i32)>,
},
Queued {
reads: Vec<BatchRead>,
first: usize,
sink: Arc<QueueSink>,
},
}
enum TrySend {
Full,
Failed(io::Error),
}
struct QueueSink {
done: mpsc::Sender<(usize, i32)>,
wake: Option<Arc<dyn crate::fs::ReadWake>>,
}
impl QueueSink {
fn deliver(&self, position: usize, result: i32) {
if self.done.send((position, result)).is_ok()
&& let Some(wake) = &self.wake
{
wake.wake();
}
}
}
enum Completion {
InBatch {
done: mpsc::SyncSender<(usize, i32)>,
position: usize,
},
InQueue {
sink: Arc<QueueSink>,
position: usize,
},
}
impl Completion {
fn deliver(self, result: i32) {
match self {
Self::InBatch { done, position } => done.send((position, result)).ok(),
Self::InQueue { sink, position } => {
sink.deliver(position, result);
None
}
};
}
}
#[cfg(test)]
#[derive(Default)]
struct SubmitCounts {
channels: core::sync::atomic::AtomicUsize,
locks: core::sync::atomic::AtomicUsize,
}
struct RingThread {
tx: Mutex<Option<mpsc::SyncSender<Submission>>>,
handle: Mutex<Option<thread::JoinHandle<()>>>,
#[cfg(test)]
counts: SubmitCounts,
}
impl RingThread {
fn spawn(sq_entries: u32) -> io::Result<Self> {
let ring = IoUring::new(sq_entries)?;
let (tx, rx) = mpsc::sync_channel(sq_entries as usize);
let handle = thread::Builder::new()
.name("lsm-io-uring".into())
.spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Self::event_loop(ring, rx);
}));
if result.is_err() {
log::error!("io_uring ring thread panicked; aborting to avoid UB");
std::process::abort();
}
})?;
Ok(Self {
tx: Mutex::new(Some(tx)),
handle: Mutex::new(Some(handle)),
#[cfg(test)]
counts: SubmitCounts::default(),
})
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[expect(
clippy::needless_pass_by_value,
reason = "rx is moved into the spawned thread — must be owned"
)]
fn event_loop(mut ring: IoUring, rx: mpsc::Receiver<Submission>) {
let mut pending = std::mem::ManuallyDrop::new(HashMap::<u64, Completion>::default());
let mut next_id: u64 = 0;
loop {
let first = if pending.is_empty() {
match rx.recv() {
Ok(op) => Some(op),
Err(mpsc::RecvError) => break,
}
} else {
match rx.try_recv() {
Ok(op) => Some(op),
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
if pending.is_empty() {
break;
}
None
}
}
};
if let Some(submission) = first {
Self::accept(&mut ring, &mut pending, &mut next_id, submission);
while let Ok(submission) = rx.try_recv() {
Self::accept(&mut ring, &mut pending, &mut next_id, submission);
}
}
if pending.is_empty() {
continue;
}
loop {
match ring.submit_and_wait(1) {
Ok(_) => break,
Err(ref e) if e.raw_os_error() == Some(4 ) => {}
Err(e) => {
log::error!(
"io_uring submit_and_wait failed: {e}; aborting process to avoid UB"
);
std::process::abort();
}
}
}
for cqe in ring.completion() {
if let Some(completion) = pending.remove(&cqe.user_data()) {
completion.deliver(cqe.result());
}
}
}
#[expect(unsafe_code, reason = "ManuallyDrop cleanup on normal exit path")]
unsafe {
std::mem::ManuallyDrop::drop(&mut pending);
}
}
fn accept(
ring: &mut IoUring,
pending: &mut HashMap<u64, Completion>,
next_id: &mut u64,
submission: Submission,
) {
match submission {
Submission::Batch { reads, done } => {
for (position, read) in reads.into_iter().enumerate() {
let completion = Completion::InBatch {
done: done.clone(),
position,
};
Self::enqueue(ring, pending, next_id, &read, completion);
}
}
Submission::Queued { reads, first, sink } => {
for (index, read) in reads.into_iter().enumerate() {
let completion = Completion::InQueue {
sink: Arc::clone(&sink),
position: first + index,
};
Self::enqueue(ring, pending, next_id, &read, completion);
}
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn enqueue(
ring: &mut IoUring,
pending: &mut HashMap<u64, Completion>,
next_id: &mut u64,
read: &BatchRead,
completion: Completion,
) {
let id = *next_id;
*next_id = next_id.wrapping_add(1);
let sqe = opcode::Read::new(types::Fd(read.fd), read.buf.0, read.len)
.offset(read.offset)
.build()
.user_data(id);
#[expect(unsafe_code, reason = "io_uring SQE push")]
unsafe {
while ring.submission().push(&sqe).is_err() {
loop {
match ring.submit_and_wait(1) {
Ok(_) => break,
Err(ref e) if e.raw_os_error() == Some(4 ) => {}
Err(e) => {
log::error!(
"io_uring submit_and_wait failed in SQ retry: {e}; aborting"
);
std::process::abort();
}
}
}
for cqe in ring.completion() {
if let Some(done) = pending.remove(&cqe.user_data()) {
done.deliver(cqe.result());
}
}
}
}
pending.insert(id, completion);
}
#[cfg_attr(
not(test),
expect(
clippy::unused_self,
reason = "self carries the test-only channel count"
)
)]
fn channel<T>(&self, bound: usize) -> (mpsc::SyncSender<T>, mpsc::Receiver<T>) {
#[cfg(test)]
self.counts
.channels
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
mpsc::sync_channel(bound)
}
fn send(&self, submission: Submission) -> io::Result<()> {
#[cfg(test)]
self.counts
.locks
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
self.tx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "io_uring thread shut down"))?
.send(submission)
.map_err(|_| io::Error::new(io::ErrorKind::BrokenPipe, "io_uring thread exited"))
}
fn try_send(&self, submission: Submission) -> Result<(), TrySend> {
#[cfg(test)]
self.counts
.locks
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
let guard = match self.tx.try_lock() {
Ok(guard) => guard,
Err(std::sync::TryLockError::Poisoned(poisoned)) => poisoned.into_inner(),
Err(std::sync::TryLockError::WouldBlock) => return Err(TrySend::Full),
};
let sent = guard.as_ref().map(|tx| tx.try_send(submission));
drop(guard);
match sent {
Some(Ok(())) => Ok(()),
Some(Err(mpsc::TrySendError::Full(_))) => Err(TrySend::Full),
Some(Err(mpsc::TrySendError::Disconnected(_))) => Err(TrySend::Failed(io::Error::new(
io::ErrorKind::BrokenPipe,
"io_uring thread exited",
))),
None => Err(TrySend::Failed(io::Error::new(
io::ErrorKind::BrokenPipe,
"io_uring thread shut down",
))),
}
}
fn submit_batch(
&self,
reads: Vec<BatchRead>,
expected: &[usize],
short_read: &'static str,
on_read: impl FnMut(usize),
) -> Result<(), (usize, io::Error)> {
if reads.is_empty() {
return Ok(());
}
let (done, results) = self.channel(reads.len());
self.send(Submission::Batch { reads, done })
.map_err(|e| (0, e))?;
drain_batch(expected, || results.recv().ok(), short_read, on_read)
}
fn submit_reads<R: super::read_buf::ReadRegion>(
&self,
fd: i32,
regions: &mut [R],
) -> io::Result<()> {
let mut lens: Vec<u32> = Vec::with_capacity(regions.len());
for region in regions.iter_mut() {
#[expect(unsafe_code, reason = "see safety")]
let (_, buf) = unsafe { region.target() };
let len = if buf.is_empty() {
0
} else {
i32::try_from(buf.len())
.map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "buffer exceeds i32::MAX")
})?
.unsigned_abs()
};
lens.push(len);
}
let mut reads = Vec::with_capacity(regions.len());
let mut expected = Vec::with_capacity(regions.len());
for (region, &len) in regions.iter_mut().zip(&lens) {
#[expect(unsafe_code, reason = "see safety")]
let (offset, buf) = unsafe { region.target() };
if buf.is_empty() {
continue;
}
reads.push(BatchRead {
fd,
buf: UnsafeSendMutPtr(buf.as_mut_ptr()),
len,
offset,
});
expected.push(buf.len());
}
self.submit_batch(
reads,
&expected,
"io_uring read_many: short read on a fixed-size block region",
|_| {},
)
.map_err(|(_, e)| e)
}
fn submit_reads_multi<'r, T: core::borrow::BorrowMut<BlockRead<'r>>>(
&self,
reqs: &mut [T],
on_read: Option<OnRead<'_>>,
) -> Result<(), (usize, io::Error)> {
let mut metas: Vec<Option<(i32, u32, u64)>> = Vec::with_capacity(reqs.len());
for (at, req) in reqs.iter().map(core::borrow::Borrow::borrow).enumerate() {
if req.buf.capacity() == 0 {
metas.push(None);
continue;
}
let fd = req.file.backing_fd().ok_or_else(|| {
(
at,
io::Error::new(
io::ErrorKind::InvalidInput,
"submit_reads_multi: request without fd",
),
)
})?;
let len: u32 = i32::try_from(req.buf.capacity() - req.buf.filled())
.map_err(|_| {
(
at,
io::Error::new(io::ErrorKind::InvalidInput, "buffer exceeds i32::MAX"),
)
})?
.unsigned_abs();
let offset = req
.offset
.checked_add(req.buf.filled() as u64)
.ok_or_else(|| {
(
at,
io::Error::new(
io::ErrorKind::InvalidInput,
"submit_reads_multi: request offset overflows",
),
)
})?;
metas.push(Some((fd, len, offset)));
}
let mut on_read = on_read;
if let Some(on_read) = on_read.as_deref_mut() {
for (at, (req, meta)) in reqs
.iter()
.map(core::borrow::Borrow::borrow)
.zip(&metas)
.enumerate()
{
if meta.is_none() {
on_read(at, req);
}
}
}
let mut reads = Vec::with_capacity(reqs.len());
let mut expected = Vec::with_capacity(reqs.len());
let mut ats = Vec::with_capacity(reqs.len());
for (at, (req, meta)) in reqs
.iter_mut()
.map(core::borrow::BorrowMut::borrow_mut)
.zip(&metas)
.enumerate()
{
let Some((fd, len, offset)) = *meta else {
continue;
};
expected.push(req.buf.capacity() - req.buf.filled());
reads.push(BatchRead {
fd,
buf: UnsafeSendMutPtr(req.buf.unfilled_mut().as_mut_ptr()),
len,
offset,
});
ats.push(at);
}
const SHORT_READ: &str = "io_uring read_blocks_batched: short read on a fixed-size block";
let blame =
|(position, e): (usize, io::Error)| (ats.get(position).copied().unwrap_or_default(), e);
let count_filled = |req: &mut BlockRead<'r>| {
let remaining = req.buf.capacity() - req.buf.filled();
req.buf.advance(remaining);
};
if let Some(on_read) = on_read {
return self
.submit_batch(reads, &expected, SHORT_READ, |position| {
if let Some(&at) = ats.get(position)
&& let Some(req) = reqs.get_mut(at)
{
let req = req.borrow_mut();
count_filled(req);
on_read(at, req);
}
})
.map_err(blame);
}
self.submit_batch(reads, &expected, SHORT_READ, |_| {})
.map_err(blame)?;
for req in reqs.iter_mut().map(core::borrow::BorrowMut::borrow_mut) {
count_filled(req);
}
Ok(())
}
}
struct UringReadQueue<'r> {
ring: &'r RingThread,
sink: Arc<QueueSink>,
done_rx: mpsc::Receiver<(usize, i32)>,
submitted: Vec<QueuedRead>,
sent: std::collections::VecDeque<Option<QueuedRead>>,
base: usize,
on_ring: usize,
finished: Vec<ReadDone>,
serial: Vec<QueuedRead>,
}
impl<'r> UringReadQueue<'r> {
fn new(ring: &'r RingThread) -> Self {
let (done, done_rx) = mpsc::channel();
Self {
ring,
sink: Arc::new(QueueSink { done, wake: None }),
done_rx,
submitted: Vec::new(),
sent: std::collections::VecDeque::new(),
base: 0,
on_ring: 0,
finished: Vec::new(),
serial: Vec::new(),
}
}
fn read_serial(&mut self) {
for mut read in core::mem::take(&mut self.serial) {
let want = read.buf.len();
let result = match read.file.read_at(&mut read.buf, read.offset) {
Ok(n) if n == want => Ok(()),
Ok(_) => Err(short_read()),
Err(error) => Err(error),
};
self.finished.push(done(read, result));
}
}
fn issue(&mut self, block: bool) {
if self.submitted.is_empty() {
return;
}
let first = self.base + self.sent.len();
let mut reads = Vec::with_capacity(self.submitted.len());
let mut deferred: Vec<(usize, QueuedRead)> = Vec::new();
let mut places: Vec<usize> = Vec::new();
for (place, mut read) in core::mem::take(&mut self.submitted).into_iter().enumerate() {
if read.buf.is_empty() {
self.finished.push(done(read, Ok(())));
continue;
}
let Some(fd) = read.file.backing_fd() else {
if block {
self.serial.push(read);
} else {
deferred.push((place, read));
}
continue;
};
let Ok(len) = i32::try_from(read.buf.len()) else {
self.finished.push(done(
read,
Err(crate::io::Error::from(io::Error::new(
io::ErrorKind::InvalidInput,
"buffer exceeds i32::MAX",
))),
));
continue;
};
reads.push(BatchRead {
fd,
buf: UnsafeSendMutPtr(read.buf.as_mut_ptr()),
len: len.unsigned_abs(),
offset: read.offset,
});
self.sent.push_back(Some(read));
places.push(place);
}
if reads.is_empty() {
self.submitted
.extend(deferred.into_iter().map(|(_, read)| read));
return;
}
let count = reads.len();
let submission = Submission::Queued {
reads,
first,
sink: Arc::clone(&self.sink),
};
let sent = if block {
self.ring.send(submission)
} else {
match self.ring.try_send(submission) {
Ok(()) => Ok(()),
Err(TrySend::Failed(error)) => Err(error),
Err(TrySend::Full) => {
let kept = self.sent.split_off(first - self.base);
let mut back: Vec<(usize, QueuedRead)> = places
.into_iter()
.zip(kept.into_iter().flatten())
.chain(deferred)
.collect();
back.sort_unstable_by_key(|(place, _)| *place);
self.submitted
.extend(back.into_iter().map(|(_, read)| read));
return;
}
}
};
self.submitted
.extend(deferred.into_iter().map(|(_, read)| read));
match sent {
Ok(()) => self.on_ring += count,
Err(error) => {
for slot in self.sent.iter_mut().skip(first - self.base) {
if let Some(read) = slot.take() {
let failure = io::Error::new(error.kind(), error.to_string());
self.finished
.push(done(read, Err(crate::io::Error::from(failure))));
}
}
self.trim();
}
}
}
fn take_sent(&mut self, position: usize) -> Option<QueuedRead> {
let read = self
.sent
.get_mut(position.checked_sub(self.base)?)?
.take()?;
self.trim();
Some(read)
}
fn trim(&mut self) {
while self.sent.front().is_some_and(Option::is_none) {
self.sent.pop_front();
self.base += 1;
}
while self.sent.back().is_some_and(Option::is_none) {
self.sent.pop_back();
}
}
fn complete(&mut self, position: usize, result: i32) -> Option<ReadDone> {
let read = self.take_sent(position)?;
self.on_ring -= 1;
let want = read.buf.len();
let result = match usize::try_from(result) {
Ok(n) if n == want => Ok(()),
Ok(_) => Err(short_read()),
Err(_) => Err(crate::io::Error::from(io::Error::from_raw_os_error(
-result,
))),
};
Some(done(read, result))
}
}
fn done(read: QueuedRead, result: crate::io::Result<()>) -> ReadDone {
ReadDone {
tag: read.tag,
buf: read.buf,
result,
}
}
fn short_read() -> crate::io::Error {
crate::io::Error::from(io::Error::new(
io::ErrorKind::UnexpectedEof,
"io_uring read queue: short read on a fixed-size block",
))
}
impl ReadQueue for UringReadQueue<'_> {
fn submit(&mut self, read: QueuedRead) {
self.submitted.push(read);
}
fn outstanding(&self) -> usize {
self.submitted.len() + self.on_ring + self.finished.len() + self.serial.len()
}
fn held(&self) -> usize {
self.serial.len() + self.submitted.len()
}
fn issues_at_wait_for_none(&self) -> bool {
true
}
fn take_kept(&mut self) -> Vec<QueuedRead> {
core::mem::take(&mut self.submitted)
}
fn wait(&mut self, min: usize, on_done: &mut dyn FnMut(ReadDone)) {
self.issue(min > 0);
if min > 0 {
self.read_serial();
}
let mut handed = 0usize;
for read in core::mem::take(&mut self.finished) {
on_done(read);
handed += 1;
}
while self.on_ring > 0 {
let next = if handed < min {
self.done_rx.recv().ok()
} else {
self.done_rx.try_recv().ok()
};
let Some((position, result)) = next else {
break;
};
if let Some(read) = self.complete(position, result) {
on_done(read);
handed += 1;
}
}
}
fn set_wake(&mut self, wake: Arc<dyn crate::fs::ReadWake>) -> bool {
if self.on_ring > 0 {
return false;
}
self.sink = Arc::new(QueueSink {
done: self.sink.done.clone(),
wake: Some(wake),
});
true
}
}
impl Drop for UringReadQueue<'_> {
fn drop(&mut self) {
while self.on_ring > 0 {
let Ok((position, _)) = self.done_rx.recv() else {
break;
};
if self.take_sent(position).is_some() {
self.on_ring -= 1;
}
}
}
}
fn drain_batch<N: FnMut() -> Option<(usize, i32)>>(
expected: &[usize],
next: N,
short_read: &'static str,
mut on_read: impl FnMut(usize),
) -> Result<(), (usize, io::Error)> {
struct Owed<N: FnMut() -> Option<(usize, i32)>> {
next: N,
count: usize,
}
impl<N: FnMut() -> Option<(usize, i32)>> Drop for Owed<N> {
fn drop(&mut self) {
while self.count > 0 && (self.next)().is_some() {
self.count -= 1;
}
}
}
let mut owed = Owed {
next,
count: expected.len(),
};
let mut results: Vec<Option<i32>> = vec![None; expected.len()];
while owed.count > 0 {
let Some((position, result)) = (owed.next)() else {
break;
};
owed.count -= 1;
if let (Some(slot), Some(&want)) = (results.get_mut(position), expected.get(position)) {
*slot = Some(result);
if usize::try_from(result).is_ok_and(|n| n == want) {
on_read(position);
}
}
}
owed.count = 0;
for (position, (result, &want)) in results.into_iter().zip(expected).enumerate() {
let failure = match result {
None => io::Error::new(io::ErrorKind::BrokenPipe, "io_uring thread exited"),
Some(result) if result < 0 => io::Error::from_raw_os_error(-result),
#[expect(clippy::cast_sign_loss, reason = "guarded by result < 0 above")]
Some(result) if result as usize != want => {
io::Error::new(io::ErrorKind::UnexpectedEof, short_read)
}
Some(_) => continue,
};
return Err((position, failure));
}
Ok(())
}
impl Drop for RingThread {
#[cfg_attr(coverage_nightly, coverage(off))]
fn drop(&mut self) {
let tx = match self.tx.get_mut() {
Ok(tx) => tx,
Err(poisoned) => poisoned.into_inner(),
};
*tx = None;
let handle_slot = match self.handle.get_mut() {
Ok(h) => h,
Err(poisoned) => poisoned.into_inner(),
};
if let Some(handle) = handle_slot.take()
&& handle.join().is_err()
{
log::error!("io_uring ring thread panicked during shutdown");
}
}
}
#[cfg(test)]
mod tests;