use core::mem::MaybeUninit;
use core::sync::atomic::AtomicU64;
use std::collections::VecDeque;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::sync::Arc;
use std::time::Duration;
use super::Notifier;
use crate::rt::shard::ring::RECOVER;
use crate::syscore::linux::iouring::ops::{NOT_AN_OP, WAKE_LOW};
use crate::syscore::linux::iouring::ring::sys::{IORING_SETUP_SINGLE_ISSUER, IORING_SETUP_SQPOLL};
use crate::syscore::linux::iouring::ring::{IoUringCqe, Ring, RingConfig, SqFull, Sqe};
pub(crate) trait CqeSink {
fn room(&self) -> usize;
fn put(&mut self, cqe: IoUringCqe);
}
impl CqeSink for Vec<IoUringCqe> {
fn room(&self) -> usize {
usize::MAX
}
fn put(&mut self, cqe: IoUringCqe) {
self.push(cqe);
}
}
pub(crate) const CQE_BATCH: usize = 32;
pub(crate) struct CqeBatch {
cqes: [MaybeUninit<IoUringCqe>; CQE_BATCH],
len: usize,
}
impl CqeBatch {
pub(crate) fn new() -> Self {
Self {
cqes: [MaybeUninit::uninit(); CQE_BATCH],
len: 0,
}
}
pub(crate) fn len(&self) -> usize {
self.len
}
pub(crate) fn is_full(&self) -> bool {
self.len == CQE_BATCH
}
pub(crate) fn get(&self, at: usize) -> IoUringCqe {
debug_assert!(at < self.len);
unsafe { self.cqes[at].assume_init() }
}
pub(crate) fn clear(&mut self) {
self.len = 0;
}
}
impl CqeSink for CqeBatch {
fn room(&self) -> usize {
CQE_BATCH - self.len
}
fn put(&mut self, cqe: IoUringCqe) {
if let Some(slot) = self.cqes.get_mut(self.len) {
slot.write(cqe);
self.len += 1;
}
}
}
static WAKE_SINK: AtomicU64 = AtomicU64::new(0);
pub(crate) struct RingPoller {
ring: Ring,
wake: Arc<OwnedFd>,
wake_armed: bool,
wake_id: u64,
waiting: VecDeque<Sqe>,
enabled: bool,
attend: bool,
overflowed: bool,
overflows: u64,
dropped_seen: u32,
entered_with_more: bool,
}
unsafe impl Send for RingPoller {}
impl RingPoller {
pub(crate) fn new(cfg: &RingConfig, files: u32) -> io::Result<Self> {
let mut cfg = *cfg;
cfg.start_disabled = true;
let ring = Ring::new(&cfg)?;
if files > 0 {
ring.register_files_sparse(files)?;
}
let wake = unsafe {
let fd = libc::eventfd(0, libc::EFD_CLOEXEC);
if fd < 0 {
return Err(io::Error::last_os_error());
}
OwnedFd::from_raw_fd(fd)
};
Ok(Self {
ring,
wake: Arc::new(wake),
wake_armed: false,
wake_id: NOT_AN_OP,
waiting: VecDeque::new(),
enabled: false,
attend: true,
overflowed: false,
overflows: 0,
dropped_seen: 0,
entered_with_more: false,
})
}
pub(crate) fn enable(&mut self) -> io::Result<()> {
if self.enabled {
return Ok(());
}
self.ring.enable()?;
self.enabled = true;
if self.ring.flags() & IORING_SETUP_SINGLE_ISSUER != 0 {
let _ = self.ring.register_ring_fd();
}
Ok(())
}
pub(crate) fn notifier(&self) -> Notifier {
Notifier::of(Arc::clone(&self.wake))
}
pub(crate) fn push(&mut self, sqe: Sqe) {
if !self.waiting.is_empty() || self.ring.push(&sqe).is_err() {
self.waiting.push_back(sqe);
}
}
pub(crate) fn submit_now(&mut self) -> io::Result<bool> {
self.enable()?;
loop {
self.flush_waiting();
match self.ring.submit() {
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
Err(err) if self.dropped_reported(&err) => continue,
Err(err) if matches!(err.raw_os_error(), Some(libc::EBUSY | libc::EAGAIN)) => {
return Ok(false);
}
Err(err) => return Err(err),
}
if self.ring.sq_ready() > 0 {
return Ok(false);
}
if self.waiting.is_empty() {
return Ok(true);
}
}
}
fn flush_waiting(&mut self) {
while let Some(sqe) = self.waiting.front() {
if let Err(SqFull) = self.ring.push(sqe) {
return;
}
self.waiting.pop_front();
}
}
pub(crate) fn sqpoll(&self) -> bool {
self.ring.flags() & IORING_SETUP_SQPOLL != 0
}
pub(crate) fn pending(&self, mut f: impl FnMut(u64)) {
self.ring.pending(&mut f);
self.waiting.iter().for_each(|sqe| f(sqe.data()));
}
pub(crate) fn adopt(&mut self, ignored: u64, pending: &[u64]) {
if !self.wake_armed || pending.binary_search(&self.wake_id).is_ok() {
return;
}
self.push(Sqe::async_cancel(self.wake_id).user_data(ignored));
self.wake_id = if self.wake_id == WAKE_LOW {
NOT_AN_OP
} else {
self.wake_id - 1
};
self.wake_armed = false;
}
fn arm_wake(&mut self) {
if self.wake_armed {
return;
}
let sqe = Sqe::read(self.wake.as_raw_fd(), WAKE_SINK.as_ptr().cast(), 8, 0)
.user_data(self.wake_id);
if self.ring.push(&sqe).is_err() {
self.waiting.push_front(sqe);
}
self.wake_armed = true;
}
pub(crate) fn wait(
&mut self,
timeout: Option<Duration>,
out: &mut impl CqeSink,
) -> io::Result<bool> {
if self.attend {
self.attend()?;
}
self.arm_wake();
self.flush_waiting();
self.look_for_overflow();
let looking =
timeout == Some(Duration::ZERO) || (!self.waiting.is_empty() && !self.submit_now()?);
let mut entered = false;
#[cfg(test)]
let report = ENTER_EBADR_FOR_TEST.with(|n| n.replace(n.get().saturating_sub(1))) > 0;
#[cfg(not(test))]
let report = false;
let result = if report {
Err(io::Error::from_raw_os_error(libc::EBADR))
} else if looking {
self.ring
.submit_or_look(self.entered_with_more)
.map(|e| entered = e)
} else {
self.ring.submit_and_wait(1, timeout).map(drop)
};
match result {
Ok(()) => {}
Err(err)
if matches!(
err.raw_os_error(),
Some(libc::EINTR) | Some(libc::EBUSY) | Some(libc::EAGAIN)
) || self.dropped_reported(&err) => {}
Err(err) => return Err(err),
}
let (woken, got) = self.reap(out);
self.entered_with_more = entered && (woken || got > 0);
self.look_for_overflow();
self.flush_waiting();
Ok(woken)
}
#[cold]
#[inline(never)]
fn attend(&mut self) -> io::Result<()> {
if !self.enabled {
self.enable()?;
}
self.dropped(false);
self.attend = false;
Ok(())
}
fn dropped_reported(&mut self, err: &io::Error) -> bool {
err.raw_os_error() == Some(libc::EBADR) && self.dropped(true)
}
#[cold]
#[inline(never)]
fn dropped(&mut self, reported: bool) -> bool {
let now = self.ring.completions_dropped();
let mut lost = now.wrapping_sub(self.dropped_seen);
if lost == 0 {
if !reported {
return false;
}
lost = 1;
}
self.dropped_seen = now;
crate::rt::counters::completions_dropped(u64::from(lost));
log::error!(
"the kernel dropped {lost} io_uring completions for want of memory, so the shard cancels its operations and ends those that do not end with an error; give the process more memory"
);
self.ask_recovery();
true
}
#[cold]
#[inline(never)]
pub(crate) fn ask_recovery(&mut self) {
self.push(Sqe::nop().user_data(RECOVER));
}
#[cfg(test)]
pub(crate) fn drop_for_test(&mut self) {
self.dropped(true);
}
pub(crate) fn reap(&mut self, out: &mut impl CqeSink) -> (bool, usize) {
let mut woken = false;
let mut got = 0;
for cqe in self.ring.completions().take(out.room()) {
if cqe.user_data >= WAKE_LOW {
woken = true;
if cqe.user_data == self.wake_id {
self.wake_armed = false;
}
} else {
out.put(cqe);
got += 1;
}
}
(woken, got)
}
fn look_for_overflow(&mut self) {
let overflowed = self.ring.cq_overflow_pending();
if overflowed != self.overflowed {
self.overflows += u64::from(overflowed);
self.overflowed = overflowed;
self.attend = true;
}
}
pub(crate) fn update_file(&self, slot: u32, fd: std::os::fd::RawFd) -> io::Result<()> {
self.ring.update_file(slot, fd)
}
pub(crate) fn overflows(&self) -> u64 {
self.overflows
}
pub(crate) fn cq_entries(&self) -> u32 {
self.ring.cq_entries()
}
pub(crate) fn ring(&self) -> &Ring {
&self.ring
}
}
#[cfg(test)]
thread_local! {
pub(crate) static ENTER_EBADR_FOR_TEST: core::cell::Cell<u32> = const { core::cell::Cell::new(0) };
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use super::RingPoller;
use crate::syscore::linux::iouring::ring::{RingConfig, Sqe};
fn poller() -> Option<RingPoller> {
let mut p = match RingPoller::new(&RingConfig::new(8), 16) {
Ok(p) => p,
Err(err) => {
eprintln!("skipping: io_uring unavailable: {err}");
return None;
}
};
p.enable().unwrap();
Some(p)
}
#[test]
fn a_wake_from_another_thread_ends_a_wait() {
let Some(mut p) = poller() else { return };
let n = p.notifier();
let waker = std::thread::spawn(move || n.notify());
let mut out = Vec::new();
let start = Instant::now();
loop {
if p.wait(Some(Duration::from_secs(10)), &mut out).unwrap() {
break;
}
assert!(start.elapsed() < Duration::from_secs(10), "no wake came");
}
waker.join().unwrap();
assert!(out.is_empty());
p.notifier().notify();
assert!(p.wait(Some(Duration::from_secs(10)), &mut out).unwrap());
}
#[test]
fn a_wake_after_its_poller_went_writes_no_memory_the_poller_freed() {
let Ok(mut p) = RingPoller::new(&RingConfig::new(8).handed_over(), 0) else {
eprintln!("skipping: io_uring unavailable");
return;
};
p.enable().unwrap();
let mut out = Vec::new();
assert!(!p.wait(Some(Duration::ZERO), &mut out).unwrap());
let kept = unsafe {
let fd = libc::dup(p.ring.as_raw_fd());
assert!(fd >= 0, "{}", std::io::Error::last_os_error());
<std::os::fd::OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(fd)
};
let notifier = p.notifier();
drop(p);
const MARK: u64 = 0x5eed_5eed_5eed_5eed;
let fresh: Vec<Box<u64>> = (0..64).map(|_| Box::new(MARK)).collect();
notifier.notify();
let mut ready = libc::pollfd {
fd: std::os::fd::AsRawFd::as_raw_fd(&kept),
events: libc::POLLIN,
revents: 0,
};
let n = unsafe { libc::poll(&mut ready, 1, 10_000) };
assert_eq!(n, 1, "the wake's read never completed");
for each in &fresh {
let now = unsafe { core::ptr::read_volatile(&**each) };
assert_eq!(now, MARK, "the wake's read wrote memory its poller freed");
}
}
#[test]
fn a_timed_wait_ends_at_its_timeout() {
let Some(mut p) = poller() else { return };
let mut out = Vec::new();
let start = Instant::now();
assert!(!p.wait(Some(Duration::from_millis(20)), &mut out).unwrap());
assert!(start.elapsed() >= Duration::from_millis(20));
}
fn flood(p: &mut RingPoller, first: u64, n: u64) {
let mut next = first;
while next < first + n {
while next < first + n && p.ring.push(&Sqe::nop().user_data(next)).is_ok() {
next += 1;
}
match p.ring.submit() {
Ok(_) => {}
Err(e) if e.raw_os_error() == Some(libc::EBUSY) => {}
Err(e) => panic!("submit failed: {e}"),
}
}
assert!(
p.ring.cq_overflow_pending(),
"the flood overflowed the queue"
);
}
#[test]
fn an_overflow_is_counted_once_and_loses_nothing() {
let Some(mut p) = poller() else { return };
const N: u64 = 64;
let mut out = Vec::new();
for round in 0..2 {
flood(&mut p, round * N, N);
assert_eq!(p.overflows(), round);
out.clear();
let deadline = Instant::now() + Duration::from_secs(10);
while out.len() < N as usize {
p.wait(Some(Duration::from_millis(100)), &mut out).unwrap();
assert!(Instant::now() < deadline, "{} of {N} completed", out.len());
}
let mut got: Vec<u64> = out.iter().map(|c| c.user_data).collect();
got.sort_unstable();
assert_eq!(got, (round * N..round * N + N).collect::<Vec<_>>());
assert_eq!(p.overflows(), round + 1);
}
}
#[test]
fn entries_past_a_full_queue_wait_and_complete_in_order() {
let Some(mut p) = poller() else { return };
for i in 0..40u64 {
p.push(Sqe::nop().user_data(i));
}
let mut out = Vec::new();
let deadline = Instant::now() + Duration::from_secs(10);
while out.len() < 40 {
p.wait(Some(Duration::from_millis(100)), &mut out).unwrap();
assert!(Instant::now() < deadline, "{} of 40 completed", out.len());
}
let got: Vec<u64> = out.iter().map(|c| c.user_data).collect();
assert_eq!(got, (0..40).collect::<Vec<_>>());
}
#[test]
fn a_wake_ends_a_wait_with_more_entries_than_the_queue_holds() {
let Some(mut p) = poller() else { return };
let mut fds = [0; 2];
assert_eq!(unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) }, 0);
let reads = p.ring.sq_entries() as usize + 4;
let mut bytes = vec![0u8; reads];
for (i, byte) in bytes.iter_mut().enumerate() {
p.push(Sqe::read(fds[0], core::ptr::from_mut(byte), 1, u64::MAX).user_data(i as u64));
}
p.notifier().notify();
let mut out = Vec::new();
assert!(
p.wait(Some(Duration::from_secs(10)), &mut out).unwrap(),
"the wake waited behind a full submission queue"
);
unsafe { libc::close(fds[1]) };
let deadline = Instant::now() + Duration::from_secs(10);
while out.len() < reads {
p.wait(Some(Duration::from_millis(100)), &mut out).unwrap();
assert!(
Instant::now() < deadline,
"{} of {reads} reads ended",
out.len()
);
}
unsafe { libc::close(fds[0]) };
}
#[test]
fn a_look_sees_completions_that_arrive_while_it_only_looks() {
use crate::syscore::linux::iouring::ring::taskrun::flag_works;
let base = RingConfig::new(8);
let modes = [
(
"deferred",
RingConfig {
taskrun_flag: flag_works(),
..base
},
),
("deferred, no flag", base),
("handed over", base.handed_over()),
(
"kernel thread",
RingConfig {
sqpoll_idle_ms: Some(50),
..base
},
),
];
for (name, cfg) in modes {
let mut p = match RingPoller::new(&cfg, 16) {
Ok(p) => p,
Err(err) => {
eprintln!("skipping {name}: {err}");
continue;
}
};
p.enable().unwrap();
let mut fds = [0; 2];
assert_eq!(unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) }, 0);
let mut byte = Box::new(0u8);
p.push(Sqe::read(fds[0], core::ptr::from_mut(&mut *byte), 1, u64::MAX).user_data(7));
let mut out = Vec::new();
assert!(!p.wait(Some(Duration::ZERO), &mut out).unwrap());
let notifier = p.notifier();
notifier.notify();
assert_eq!(unsafe { libc::write(fds[1], [1u8].as_ptr().cast(), 1) }, 1);
let (mut woken, deadline) = (false, Instant::now() + Duration::from_secs(10));
while !woken || out.is_empty() {
woken |= p.wait(Some(Duration::ZERO), &mut out).unwrap();
assert!(
Instant::now() < deadline,
"{name}: woken {woken}, {} completions",
out.len()
);
std::thread::yield_now();
}
assert_eq!(out[0].user_data, 7, "{name}");
unsafe { (libc::close(fds[0]), libc::close(fds[1])) };
}
}
#[test]
fn an_idle_look_makes_no_enter_where_the_kernel_flags_task_work() {
use crate::syscore::linux::iouring::ring::taskrun::flag_works;
if !flag_works() {
eprintln!("skipping: this kernel does not flag task work");
return;
}
let cfg = RingConfig {
taskrun_flag: true,
..RingConfig::new(8)
};
let Ok(mut p) = RingPoller::new(&cfg, 16) else {
return;
};
p.enable().unwrap();
let mut out = Vec::new();
p.wait(Some(Duration::ZERO), &mut out).unwrap();
assert!(!p.ring.submit_or_look(false).unwrap());
p.notifier().notify();
assert!(p.ring.taskrun_pending());
assert!(p.ring.submit_or_look(false).unwrap());
}
}