#![allow(unsafe_code)]
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use io_uring::IoUring;
use parking_lot::Mutex;
use crate::Result;
use crate::errors::PagedbError;
use crate::vfs::traits::checked_indexed_completion;
pub(crate) const RING_DEPTH: u32 = 4096;
pub(crate) enum SubmitError {
Settled(std::io::Error),
Abandoned(std::io::Error),
}
impl SubmitError {
pub(crate) fn into_io(self) -> std::io::Error {
match self {
Self::Settled(error) | Self::Abandoned(error) => error,
}
}
}
pub(crate) struct SharedRing {
ring: Mutex<IoUring>,
poisoned: AtomicBool,
}
impl SharedRing {
fn poison(&self) {
self.poisoned.store(true, Ordering::Release);
}
pub(crate) unsafe fn submit_and_collect(
&self,
entries: &[io_uring::squeue::Entry],
) -> std::result::Result<Vec<i32>, SubmitError> {
if entries.is_empty() {
return Ok(Vec::new());
}
if self.poisoned.load(Ordering::Acquire) {
return Err(SubmitError::Settled(poisoned_error()));
}
let total = entries.len();
let chunk_size = RING_DEPTH as usize;
let mut results = vec![0i32; total];
let mut guard = self.ring.lock();
let mut base = 0usize;
while base < total {
let end = (base + chunk_size).min(total);
let chunk_len = end - base;
let pushed = {
let mut sq = guard.submission();
let mut pushed = 0usize;
for (index, entry) in entries[base..end].iter().enumerate() {
let tagged = entry.clone().user_data(index as u64);
if unsafe { sq.push(&tagged) }.is_err() {
break;
}
pushed += 1;
}
pushed
};
let drained = match drain_exact(&mut guard, pushed) {
Ok(drained) => drained,
Err(error) => {
self.poison();
return Err(SubmitError::Abandoned(error));
}
};
for (index, result) in drained.into_iter().enumerate() {
results[base + index] = result;
}
if pushed < chunk_len {
return Err(SubmitError::Settled(std::io::Error::other(
"io_uring submission queue full",
)));
}
base = end;
}
Ok(results)
}
}
fn drain_exact(ring: &mut IoUring, want: usize) -> std::io::Result<Vec<i32>> {
if want == 0 {
return Ok(Vec::new());
}
let mut slots: Vec<Option<i32>> = vec![None; want];
let mut found = 0usize;
while found < want {
match ring.submit_and_wait(want - found) {
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
}
let mut completion = ring.completion();
completion.sync();
for cqe in completion.by_ref() {
match checked_indexed_completion(&mut slots, cqe.user_data(), cqe.result()) {
Ok(true) => found += 1,
Ok(false) => {}
Err(error) => return Err(std::io::Error::other(error.to_string())),
}
if found == want {
break;
}
}
}
slots
.into_iter()
.map(|slot| {
slot.ok_or_else(|| std::io::Error::other("io_uring: missing indexed CQE result"))
})
.collect()
}
fn poisoned_error() -> std::io::Error {
std::io::Error::other(
"io_uring ring poisoned: an earlier operation left submissions unaccounted for; \
reopen the store to get a fresh ring",
)
}
pub struct Ring {
pub inner: Arc<SharedRing>,
}
impl Ring {
pub fn new() -> Result<Self> {
let ring = IoUring::new(RING_DEPTH).map_err(setup_failed)?;
Ok(Self {
inner: Arc::new(SharedRing {
ring: Mutex::new(ring),
poisoned: AtomicBool::new(false),
}),
})
}
}
fn setup_failed(error: std::io::Error) -> PagedbError {
PagedbError::Io(std::io::Error::new(
error.kind(),
RingSetupError { source: error },
))
}
#[derive(Debug)]
struct RingSetupError {
source: std::io::Error,
}
impl std::fmt::Display for RingSetupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"io_uring setup failed ({}); likely causes: RLIMIT_MEMLOCK too low \
for another ring (each ring is charged against it), a seccomp \
profile blocking io_uring_setup, or a kernel older than 5.1",
self.source
)
}
}
impl std::error::Error for RingSetupError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}