use std::cell::RefCell;
use std::rc::{Rc, Weak};
use std::sync::atomic::Ordering;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use io_uring::{EnterFlags, IoUring, opcode, types};
use crate::metrics::Metrics;
use crate::park::{FUTEX_BITSET_MATCH_ANY, FUTEX2_PRIVATE, FUTEX2_SIZE_U32, PARKED, RUNNING, Unpark};
use crate::shared::{Cqe, Op, Shared, Task};
use crate::{Error, timer, udp};
const SQ_ENTRIES: u32 = 256;
const CQ_ENTRIES: u32 = 2048;
const TEARDOWN_CQE_BATCH: usize = 64;
const TEARDOWN_EINTR_RETRIES: usize = 8;
const TEARDOWN_TIMEOUT: Duration = Duration::from_millis(3200);
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct Config {
pub metrics: Metrics,
}
impl Clone for Config {
fn clone(&self) -> Self {
Self {
metrics: Metrics::default(),
}
}
}
pub struct Worker {
shared: Rc<Shared>,
tasks: kio::Tasks<Task>,
park: kio::Park,
cqes: Vec<Cqe>,
futex_armed: bool,
}
impl Worker {
pub fn new(config: Config) -> Result<Self, Error> {
let Config { metrics } = config;
let metrics = metrics.counters().clone();
let ring = IoUring::builder()
.setup_single_issuer()
.setup_defer_taskrun()
.setup_coop_taskrun()
.setup_cqsize(CQ_ENTRIES)
.build(SQ_ENTRIES)
.map_err(|err| match err.raw_os_error() {
Some(libc::ENOSYS) | Some(libc::EPERM) | Some(libc::EACCES) | Some(libc::EINVAL) => {
Error::Unsupported(format!(
"io_uring is unavailable ({err}); kernel {} (Linux 6.12+ required, and container seccomp \
policies such as Docker's default commonly block io_uring)",
kernel_release()
))
}
_ => Error::ring(err),
})?;
if !ring.params().is_feature_min_timeout() {
return Err(Error::Unsupported(format!(
"kernel {} is too old: moq-uring requires Linux 6.12+ (io_uring MIN_TIMEOUT feature missing)",
kernel_release()
)));
}
Ok(Self {
shared: Rc::new(Shared {
ring: RefCell::new(ring),
ops: RefCell::new(slab::Slab::new()),
timers: Rc::new(RefCell::new(timer::Heap::new(metrics.clone()))),
spawns: RefCell::new(Vec::new()),
unpark: Unpark::new(metrics.clone()),
metrics,
next_bgid: std::cell::Cell::new(0),
stopped: std::cell::Cell::new(false),
spill: RefCell::new(std::collections::VecDeque::new()),
}),
tasks: kio::Tasks::new(),
park: kio::Park::default(),
cqes: Vec::new(),
futex_armed: false,
})
}
pub fn handle(&self) -> Handle {
Handle {
shared: self.shared.clone(),
}
}
pub fn block_on<F: Future>(&mut self, future: F) -> Result<F::Output, Error> {
let mut future = std::pin::pin!(future);
let waker = self.shared.unpark.waker();
loop {
let spawns = std::mem::take(&mut *self.shared.spawns.borrow_mut());
for task in spawns {
self.tasks.push(task);
}
let cx = Context::from_waker(&waker);
let waiter = self.park.hold(&cx);
if let Poll::Ready(value) = waiter.poll_future(future.as_mut()) {
return Ok(value);
}
let _ = self.tasks.poll(waiter);
self.shared.timers.borrow_mut().fire(Instant::now());
self.pump()?;
self.maybe_park()?;
}
}
fn pump(&mut self) -> Result<(), Error> {
self.pump_inner(None)
}
fn pump_until(&mut self, deadline: Instant) -> Result<(), Error> {
self.pump_inner(Some(deadline))
}
fn pump_inner(&mut self, deadline: Option<Instant>) -> Result<(), Error> {
self.submit()?;
loop {
if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
return Ok(());
}
self.cqes.clear();
{
let mut ring = self.shared.ring.borrow_mut();
let mut spill = self.shared.spill.borrow_mut();
let limit = deadline.map_or(usize::MAX, |_| TEARDOWN_CQE_BATCH);
let spilled = spill.len().min(limit);
self.cqes.extend(spill.drain(..spilled));
self.cqes
.extend(ring.completion().take(limit - spilled).map(|entry| Cqe {
user_data: entry.user_data(),
result: entry.result(),
flags: entry.flags(),
}));
}
self.shared.metrics.completions.add(self.cqes.len() as u64);
if self.cqes.is_empty() || !self.dispatch_batch(deadline, Instant::now) {
return Ok(());
}
}
}
fn dispatch_batch(&mut self, deadline: Option<Instant>, mut now: impl FnMut() -> Instant) -> bool {
for index in 0..self.cqes.len() {
if deadline.is_some_and(|deadline| now() >= deadline) {
return false;
}
let cqe = self.cqes[index];
self.dispatch(cqe);
}
true
}
fn submit(&mut self) -> Result<(), Error> {
let mut ring = self.shared.ring.borrow_mut();
if ring.submission().is_empty() {
return Ok(());
}
self.shared.metrics.enters.add(1);
match ring.submit() {
Ok(count) => {
self.shared.metrics.submissions.add(count as u64);
Ok(())
}
Err(err) if err.raw_os_error() == Some(libc::EINTR) => Ok(()),
Err(err) if err.raw_os_error() == Some(libc::EBUSY) => Ok(()),
Err(err) => Err(err.into()),
}
}
fn submit_teardown(&mut self) -> Result<(), Error> {
let mut ring = self.shared.ring.borrow_mut();
let mut interruptions = 0;
loop {
if ring.submission().is_empty() {
return Ok(());
}
self.shared.metrics.enters.add(1);
match ring.submit() {
Ok(0) => {
return Err(std::io::Error::other("io_uring teardown submission made no progress").into());
}
Ok(count) => self.shared.metrics.submissions.add(count as u64),
Err(err) => retry_teardown_submit(&mut interruptions, err)?,
}
}
}
fn drain_teardown(&mut self, deadline: Instant) {
let submission_failed = self.submit_teardown().is_err();
if !submission_failed {
loop {
if self.shared.ops.borrow().is_empty() {
return;
}
if Instant::now() >= deadline || self.pump_until(deadline).is_err() {
break;
}
let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
break;
};
let ring = self.shared.ring.borrow_mut();
let wait = remaining.min(std::time::Duration::from_millis(50));
let ts = types::Timespec::from(wait);
let args = types::SubmitArgs::new().timespec(&ts);
self.shared.metrics.enters.add(1);
let _ = ring.submitter().submit_with_args(1, &args);
}
}
if !self.shared.ops.borrow().is_empty() {
tracing::error!("dropping an io_uring worker with operations stuck in flight; leaking them");
std::mem::forget(std::mem::take(&mut *self.shared.ops.borrow_mut()));
}
}
fn dispatch(&mut self, cqe: Cqe) {
let key = cqe.user_data as usize;
enum Route {
Live(Rc<udp::SockShared>),
Done(Op),
}
let route = {
let mut ops = self.shared.ops.borrow_mut();
let Some(op) = ops.get(key) else {
tracing::error!(key, "completion for an unknown operation");
return;
};
let terminal = match op {
Op::Recv { .. } => cqe.result < 0 || !io_uring::cqueue::more(cqe.flags),
_ => true,
};
if terminal {
Route::Done(ops.remove(key))
} else {
match op {
Op::Recv { sock, .. } => Route::Live(sock.clone()),
_ => unreachable!("only receives are non-terminal"),
}
}
};
match route {
Route::Live(sock) => udp::on_recv(&self.shared, &sock, None, cqe, false),
Route::Done(Op::Recv { sock, one }) => udp::on_recv(&self.shared, &sock, one, cqe, true),
Route::Done(Op::Send(op)) => udp::on_send(op, cqe),
Route::Done(Op::FutexWait) => self.futex_armed = false,
Route::Done(Op::Cancel) => {}
}
}
fn maybe_park(&mut self) -> Result<(), Error> {
let unpark = self.shared.unpark.clone();
if unpark
.word
.compare_exchange(RUNNING, PARKED, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
unpark.word.store(RUNNING, Ordering::Release);
return Ok(());
}
if !self.futex_armed {
let key = self.shared.insert(Op::FutexWait);
let entry = opcode::FutexWait::new(
unpark.word.as_ptr(),
PARKED as u64,
FUTEX_BITSET_MATCH_ANY,
FUTEX2_SIZE_U32 | FUTEX2_PRIVATE,
)
.build()
.user_data(key);
if let Err(err) = self.shared.push(&entry) {
self.shared.ops.borrow_mut().remove(key as usize);
unpark.word.store(RUNNING, Ordering::Release);
return Err(err.into());
}
self.futex_armed = true;
}
let deadline = self.shared.timers.borrow().next();
self.shared.metrics.parks.add(1);
self.shared.metrics.enters.add(1);
let result = {
let mut ring = self.shared.ring.borrow_mut();
let to_submit = ring.submission().len() as u32;
let submitter = ring.submitter();
match deadline {
None => submitter.submit_and_wait(1),
Some(at) => {
let ts = abs_timespec(at);
let args = types::SubmitArgs::new().timespec(&ts);
let flags = EnterFlags::GETEVENTS | EnterFlags::EXT_ARG | EnterFlags::ABS_TIMER;
unsafe { submitter.enter(to_submit, 1, flags.bits(), Some(&args)) }
}
}
};
unpark.word.store(RUNNING, Ordering::Release);
match result {
Ok(count) => {
self.shared.metrics.submissions.add(count as u64);
Ok(())
}
Err(err)
if matches!(
err.raw_os_error(),
Some(libc::ETIME) | Some(libc::EINTR) | Some(libc::EBUSY)
) =>
{
Ok(())
}
Err(err) => Err(err.into()),
}
}
}
impl Drop for Worker {
fn drop(&mut self) {
self.shared.stopped.set(true);
let deadline = Instant::now() + TEARDOWN_TIMEOUT;
let cancel: Vec<u64> = self
.shared
.ops
.borrow()
.iter()
.filter_map(|(key, op)| matches!(op, Op::Recv { .. } | Op::FutexWait).then_some(key as u64))
.collect();
let mut cancellation_failed = false;
for key in cancel {
if Instant::now() >= deadline {
cancellation_failed = true;
break;
}
cancellation_failed |= self.shared.cancel_until(key, deadline).is_err();
}
if cancellation_failed {
tracing::error!("failed to queue one or more io_uring teardown cancellations");
}
self.drain_teardown(deadline);
}
}
impl std::fmt::Debug for Worker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Worker").field("tasks", &self.tasks.len()).finish()
}
}
pub struct Handle {
shared: Rc<Shared>,
}
impl Clone for Handle {
fn clone(&self) -> Self {
Self {
shared: self.shared.clone(),
}
}
}
impl Handle {
pub fn metrics(&self) -> Metrics {
Metrics::from_counters(self.shared.metrics.clone())
}
pub fn spawn(&self, future: impl Future<Output = ()> + 'static) {
if self.shared.stopped.get() {
return;
}
let mut future = Box::pin(future);
self.shared
.spawns
.borrow_mut()
.push(Box::new(move |waiter: &kio::Waiter| {
waiter.poll_future(future.as_mut())
}));
self.shared.unpark.unpark();
}
pub fn udp(&self, socket: impl Into<udp::Bound>, config: udp::Config) -> Result<udp::Socket, Error> {
if self.shared.stopped.get() {
return Err(Shared::gone_error().into());
}
udp::Socket::bind(&self.shared, socket.into(), config)
}
}
#[derive(Clone)]
pub(crate) struct Owner {
shared: Weak<Shared>,
timers: Rc<RefCell<timer::Heap>>,
}
impl Owner {
pub(crate) fn new(shared: &Rc<Shared>) -> Self {
Self {
shared: Rc::downgrade(shared),
timers: shared.timers.clone(),
}
}
pub fn upgrade(&self) -> Option<Rc<Shared>> {
self.shared.upgrade()
}
pub fn handle(&self) -> Option<Handle> {
let shared = self.shared.upgrade()?;
(!shared.stopped.get()).then_some(Handle { shared })
}
pub fn spawn(&self, future: impl Future<Output = ()> + 'static) {
if let Some(handle) = self.handle() {
handle.spawn(future);
}
}
pub fn timer(&self) -> crate::Timer {
crate::Timer::from_heap(self.timers.clone())
}
pub fn after(&self, duration: Duration) -> crate::Timer {
let mut timer = self.timer();
timer.set(Instant::now().checked_add(duration));
timer
}
}
impl std::fmt::Debug for Handle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Handle").finish()
}
}
impl Handle {
pub fn timer(&self) -> crate::Timer {
crate::Timer::from_heap(self.shared.timers.clone())
}
pub async fn run<D: moq_net::time::Driver>(&self, mut driver: D) -> moq_net::Error {
let mut timer = self.timer();
kio::wait(|waiter| {
loop {
match driver.poll(Instant::now(), waiter) {
Ok(at) => timer.set(at),
Err(err) => return Poll::Ready(err),
}
if timer.poll(waiter).is_pending() {
return Poll::Pending;
}
}
})
.await
}
}
fn kernel_release() -> String {
let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
if unsafe { libc::uname(&mut uts) } != 0 {
return "unknown".into();
}
unsafe { std::ffi::CStr::from_ptr(uts.release.as_ptr()) }
.to_string_lossy()
.into_owned()
}
fn abs_timespec(at: Instant) -> types::Timespec {
let delta = at.saturating_duration_since(Instant::now());
let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) };
let nanos = now.tv_nsec as u64 + delta.subsec_nanos() as u64;
let secs = (now.tv_sec as u64)
.saturating_add(delta.as_secs())
.saturating_add(nanos / 1_000_000_000);
types::Timespec::new().sec(secs).nsec((nanos % 1_000_000_000) as u32)
}
fn retry_teardown_submit(interruptions: &mut usize, err: std::io::Error) -> std::io::Result<()> {
if err.raw_os_error() != Some(libc::EINTR) || *interruptions >= TEARDOWN_EINTR_RETRIES {
return Err(err);
}
*interruptions += 1;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Timer as Deadline;
use std::time::Duration;
fn worker() -> Option<Worker> {
worker_with(Config::default())
}
fn worker_with(config: Config) -> Option<Worker> {
match Worker::new(config) {
Ok(worker) => Some(worker),
Err(Error::Unsupported(reason)) => {
eprintln!("skipping io_uring test: {reason}");
None
}
Err(err) => panic!("worker setup failed: {err}"),
}
}
#[test]
fn cloned_config_has_fresh_metrics() {
let config = Config::default();
let clone = config.clone();
assert!(!std::sync::Arc::ptr_eq(
config.metrics.counters(),
clone.metrics.counters()
));
}
#[test]
fn ready_future() {
let Some(mut worker) = worker() else { return };
let value = worker.block_on(async { 7 }).unwrap();
assert_eq!(value, 7);
}
#[test]
fn spawned_tasks_run() {
let Some(mut worker) = worker() else { return };
let handle = worker.handle();
let flag = Rc::new(std::cell::Cell::new(0));
for index in 0..3 {
let flag = flag.clone();
handle.spawn(async move {
flag.set(flag.get() + index + 1);
});
}
let handle2 = handle.clone();
worker
.block_on(async move {
Deadline::after(&handle2, Duration::from_millis(10)).wait().await;
})
.unwrap();
assert_eq!(flag.get(), 6);
}
#[test]
fn deadline_fires_at_park() {
let Some(mut worker) = worker() else { return };
let handle = worker.handle();
let start = Instant::now();
worker
.block_on(async move {
Deadline::after(&handle, Duration::from_millis(50)).wait().await;
})
.unwrap();
let elapsed = start.elapsed();
assert!(elapsed >= Duration::from_millis(50), "woke early: {elapsed:?}");
assert!(elapsed < Duration::from_secs(5), "woke far too late: {elapsed:?}");
}
#[test]
fn timer_rearm_and_disarm() {
let Some(mut worker) = worker() else { return };
let handle = worker.handle();
let mut timer = handle.timer();
assert!(timer.poll(&kio::Waiter::noop()).is_pending());
timer.set(Some(Instant::now() - Duration::from_millis(1)));
assert!(timer.poll(&kio::Waiter::noop()).is_ready());
assert!(timer.poll(&kio::Waiter::noop()).is_ready());
timer.set(Some(Instant::now() + Duration::from_secs(60)));
assert!(timer.poll(&kio::Waiter::noop()).is_pending());
timer.set(None);
assert!(timer.poll(&kio::Waiter::noop()).is_pending());
let start = Instant::now();
worker
.block_on(async move {
timer.set(Some(Instant::now() + Duration::from_millis(20)));
kio::wait(|waiter| timer.poll(waiter)).await;
})
.unwrap();
assert!(start.elapsed() >= Duration::from_millis(20));
}
#[test]
fn dropped_worker_rejects_operations() {
let Some(worker) = worker() else { return };
let handle = worker.handle();
let bind = || std::net::UdpSocket::bind("127.0.0.1:0").expect("bind");
let sock = handle.udp(bind(), udp::Config::default()).expect("socket");
let shared = sock.downgrade();
let to = sock.local_addr().expect("addr");
let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
panic!("no tx buffer");
};
drop(worker);
assert!(handle.udp(bind(), udp::Config::default()).is_err());
assert!(matches!(sock.poll_recv(&kio::Waiter::noop()), Poll::Ready(Err(_))));
assert!(matches!(sock.poll_acquire(&kio::Waiter::noop()), Poll::Ready(Err(_))));
assert!(
tx.send(udp::Transmit {
to,
len: 1200,
segment: 1200,
ecn: None,
})
.is_err()
);
handle.spawn(async {});
drop(sock);
assert!(shared.upgrade().is_none(), "the worker leaked its staged receive");
}
#[test]
fn teardown_stops_between_completions_at_the_deadline() {
let Some(mut worker) = worker() else { return };
let first = worker.shared.insert(Op::Cancel);
let second = worker.shared.insert(Op::Cancel);
let cqe = |user_data| Cqe {
user_data,
result: 0,
flags: 0,
};
let before = Instant::now();
let deadline = before + Duration::from_millis(1);
let mut now = [before, deadline].into_iter();
worker.cqes = vec![cqe(first), cqe(second)];
assert!(!worker.dispatch_batch(Some(deadline), || {
now.next().expect("one deadline check per completion")
}));
assert!(!worker.shared.ops.borrow().contains(first as usize));
assert!(worker.shared.ops.borrow().contains(second as usize));
worker.shared.ops.borrow_mut().remove(second as usize);
}
#[test]
fn expired_teardown_submits_residual_sqes() {
let Some(mut worker) = worker() else { return };
for _ in 0..SQ_ENTRIES {
worker.shared.push(&opcode::Nop::new().build()).expect("stage NOP");
}
assert_eq!(worker.shared.ring.borrow_mut().submission().len(), SQ_ENTRIES as usize);
worker.drain_teardown(Instant::now());
assert!(worker.shared.ring.borrow_mut().submission().is_empty());
}
#[test]
fn teardown_submit_interrupt_budget_is_finite() {
let interrupted = || std::io::Error::from_raw_os_error(libc::EINTR);
let mut interruptions = 0;
for _ in 0..TEARDOWN_EINTR_RETRIES {
retry_teardown_submit(&mut interruptions, interrupted()).expect("retry interrupted submit");
}
assert_eq!(interruptions, TEARDOWN_EINTR_RETRIES);
assert_eq!(
retry_teardown_submit(&mut interruptions, interrupted())
.expect_err("interrupt budget must be finite")
.raw_os_error(),
Some(libc::EINTR)
);
}
#[test]
fn dropped_worker_drains_more_receives_than_the_submission_queue() {
let Some(worker) = worker() else { return };
let handle = worker.handle();
let config = udp::Config {
gro: false,
gso: false,
multishot: false,
rx_buffers_max: 1,
rx_buffer_len: 2048,
tx_buffers_max: 1,
tx_buffer_len: 2048,
};
let mut sockets = Vec::new();
let mut shared = Vec::new();
for _ in 0..=SQ_ENTRIES {
let sock = handle
.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config.clone())
.expect("socket");
shared.push(sock.downgrade());
sockets.push(sock);
}
drop(worker);
drop(sockets);
assert!(
shared.iter().all(|shared| shared.upgrade().is_none()),
"the worker leaked a receive staged across submission batches"
);
}
#[test]
fn cq_covers_the_default_pool_ceilings() {
let config = udp::Config::default();
let per_socket = u32::from(config.tx_buffers_max) + u32::from(config.rx_buffers_max);
assert!(CQ_ENTRIES > per_socket, "CQ_ENTRIES fell behind the pool defaults");
}
#[test]
fn the_ring_honors_the_requested_cq_depth() {
let Some(worker) = worker() else { return };
let cq = worker.shared.ring.borrow().params().cq_entries();
assert!(cq >= CQ_ENTRIES, "kernel granted a {cq}-entry CQ, wanted {CQ_ENTRIES}");
}
#[test]
fn completion_overflow_is_survivable() {
let Some(mut worker) = worker() else { return };
let handle = worker.handle();
let ceiling = (CQ_ENTRIES * 2) as u16;
let config = udp::Config {
tx_buffers_max: ceiling,
tx_buffer_len: 2048,
..Default::default()
};
let sock = handle
.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
.expect("socket");
let to = sock.local_addr().expect("addr");
let mut held = Vec::new();
while let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) {
held.push(tx);
}
assert_eq!(held.len(), usize::from(ceiling));
for tx in held.drain(..) {
tx.send(udp::Transmit {
to,
len: 1200,
segment: 1200,
ecn: None,
})
.expect("send");
}
let saw_overflow = |worker: &Worker| worker.shared.ring.borrow_mut().submission().cq_overflow();
let mut overflowed = saw_overflow(&worker);
let deadline = Instant::now() + Duration::from_secs(10);
loop {
overflowed = overflowed || saw_overflow(&worker);
let h = handle.clone();
worker
.block_on(async move {
Deadline::after(&h, Duration::from_millis(10)).wait().await;
})
.unwrap();
let mut free = Vec::new();
loop {
match sock.poll_acquire(&kio::Waiter::noop()) {
Poll::Ready(Ok(tx)) => free.push(tx),
Poll::Ready(Err(err)) => panic!("send path failed: {err}"),
Poll::Pending => break,
}
}
if free.len() == usize::from(ceiling) {
break;
}
assert!(
Instant::now() < deadline,
"buffers stuck in flight: {} of {ceiling} free",
free.len()
);
}
assert!(overflowed, "the burst never overflowed the CQ; it proves nothing");
while let Poll::Ready(result) = sock.poll_recv(&kio::Waiter::noop()) {
result.expect("receive path failed");
}
}
#[test]
fn oversized_receive_pool_is_rejected() {
let Some(worker) = worker() else { return };
let handle = worker.handle();
let config = udp::Config {
rx_buffers_max: u16::MAX,
..Default::default()
};
let err = handle
.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
.expect_err("oversized pool");
assert!(matches!(err, Error::Io(err) if err.kind() == std::io::ErrorKind::InvalidInput));
}
#[test]
fn the_send_pool_grows_to_its_ceiling() {
let Some(worker) = worker() else { return };
let handle = worker.handle();
let config = udp::Config {
tx_buffers_max: u16::MAX,
..Default::default()
};
handle
.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
.expect("socket");
let config = udp::Config {
tx_buffers_max: 200,
tx_buffer_len: 4096,
..Default::default()
};
let sock = handle
.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
.expect("socket");
let mut held = Vec::new();
while let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) {
held.push(tx);
}
assert_eq!(held.len(), 200);
drop(worker);
}
#[test]
fn ungso_send_is_not_capped_at_a_train() {
let Some(worker) = worker() else { return };
let handle = worker.handle();
let config = udp::Config {
gso: false,
..Default::default()
};
let sock = handle
.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
.expect("socket");
let to = sock.local_addr().expect("addr");
let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
panic!("no tx buffer");
};
tx.send(udp::Transmit {
to,
len: 64 * 1024,
segment: 1000,
ecn: None,
})
.expect("send 66 datagrams");
drop(worker);
}
#[test]
fn ungso_send_is_capped_by_the_ring() {
let Some(worker) = worker() else { return };
let handle = worker.handle();
let config = udp::Config {
gso: false,
..Default::default()
};
let sock = handle
.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
.expect("socket");
let to = sock.local_addr().expect("addr");
let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
panic!("no tx buffer");
};
let err = tx
.send(udp::Transmit {
to,
len: 64 * 1024,
segment: 1,
ecn: None,
})
.expect_err("65536 datagrams from one buffer");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
drop(worker);
}
#[test]
fn oversized_gso_segment_is_rejected() {
let Some(worker) = worker() else { return };
let handle = worker.handle();
let sock = handle
.udp(
std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"),
udp::Config::default(),
)
.expect("socket");
let to = sock.local_addr().expect("addr");
let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
panic!("no tx buffer");
};
let err = tx
.send(udp::Transmit {
to,
len: 60_000,
segment: usize::from(u16::MAX) + 2,
ecn: None,
})
.expect_err("oversized segment");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
drop(worker);
}
#[test]
fn metrics_record_ring_and_socket_activity() {
let metrics = Metrics::default();
let config = Config {
metrics: metrics.clone(),
..Default::default()
};
let Some(mut worker) = worker_with(config) else { return };
let handle = worker.handle();
let sock = handle
.udp(
std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"),
udp::Config::default(),
)
.expect("socket");
let to = sock.local_addr().expect("addr");
let Poll::Ready(Ok(mut tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
panic!("no tx buffer");
};
tx[..4 * 1200].fill(7);
tx.send(udp::Transmit {
to,
len: 4 * 1200,
segment: 1200,
ecn: None,
})
.expect("send");
let deadline = Instant::now() + Duration::from_secs(5);
let mut received = 0;
while received == 0 && Instant::now() < deadline {
let handle = handle.clone();
worker
.block_on(async move {
Deadline::after(&handle, Duration::from_millis(10)).wait().await;
})
.unwrap();
while let Poll::Ready(packet) = sock.poll_recv(&kio::Waiter::noop()) {
let packet = packet.expect("receive path failed");
received += packet.payload().len();
}
}
assert!(received > 0, "the loopback never delivered the send");
let snap = metrics.snapshot();
assert_eq!(snap.tx_sends, 1, "one GSO train is one sendmsg: {snap:?}");
assert_eq!(snap.tx_datagrams, 4, "four segments: {snap:?}");
assert!(snap.rx_receives > 0, "no receive completions: {snap:?}");
assert!(
snap.rx_datagrams >= snap.rx_receives,
"fewer datagrams than receives: {snap:?}"
);
assert!(snap.submissions > 0, "nothing was submitted: {snap:?}");
assert!(snap.completions > 0, "nothing completed: {snap:?}");
assert!(snap.enters > 0, "the ring was never entered: {snap:?}");
assert!(snap.parks > 0, "the worker never parked: {snap:?}");
assert!(snap.timers_fired > 0, "the park deadlines never fired: {snap:?}");
let own = handle.metrics().snapshot();
assert_eq!((own.tx_sends, own.tx_datagrams), (snap.tx_sends, snap.tx_datagrams));
}
#[test]
fn metrics_record_pool_backpressure() {
let metrics = Metrics::default();
let config = Config {
metrics: metrics.clone(),
..Default::default()
};
let Some(mut worker) = worker_with(config) else { return };
let handle = worker.handle();
let sock = handle
.udp(
std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"),
udp::Config {
gro: false,
gso: false,
multishot: false,
rx_buffers_max: 1,
rx_buffer_len: 2048,
tx_buffers_max: 1,
tx_buffer_len: 2048,
},
)
.expect("socket");
let to = sock.local_addr().expect("addr");
let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
panic!("no tx buffer");
};
assert!(sock.poll_acquire(&kio::Waiter::noop()).is_pending());
assert!(sock.poll_acquire(&kio::Waiter::noop()).is_pending());
assert_eq!(metrics.snapshot().tx_stalls, 1);
tx.send(udp::Transmit {
to,
len: 1200,
segment: 1200,
ecn: None,
})
.expect("send");
let deadline = Instant::now() + Duration::from_secs(5);
let mut held = None;
while held.is_none() && Instant::now() < deadline {
let handle = handle.clone();
worker
.block_on(async move {
Deadline::after(&handle, Duration::from_millis(10)).wait().await;
})
.unwrap();
if let Poll::Ready(packet) = sock.poll_recv(&kio::Waiter::noop()) {
held = Some(packet.expect("receive path failed"));
}
}
assert!(held.is_some(), "the loopback never delivered the send");
assert!(
metrics.snapshot().rx_exhausted > 0,
"a re-arm with every buffer held went unreported: {:?}",
metrics.snapshot()
);
let Poll::Ready(Ok(_tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
panic!("completed tx buffer was not released");
};
assert!(sock.poll_acquire(&kio::Waiter::noop()).is_pending());
assert!(sock.poll_acquire(&kio::Waiter::noop()).is_pending());
assert_eq!(metrics.snapshot().tx_stalls, 2);
}
#[test]
fn metrics_count_timer_churn() {
let metrics = Metrics::default();
let config = Config {
metrics: metrics.clone(),
..Default::default()
};
let Some(worker) = worker_with(config) else { return };
let handle = worker.handle();
let mut timer = handle.timer();
timer.set(Some(Instant::now() + Duration::from_secs(60)));
assert_eq!(metrics.snapshot().timers_active(), 1);
timer.set(Some(Instant::now() + Duration::from_secs(60)));
let snap = metrics.snapshot();
assert_eq!((snap.timers_armed, snap.timers_cancelled, snap.timers_fired), (2, 1, 0));
assert_eq!(snap.timers_active(), 1);
timer.set(Some(Instant::now() - Duration::from_millis(1)));
assert!(timer.poll(&kio::Waiter::noop()).is_ready());
let snap = metrics.snapshot();
assert_eq!((snap.timers_armed, snap.timers_cancelled, snap.timers_fired), (3, 2, 1));
assert_eq!(snap.timers_active(), 0);
timer.set(Some(Instant::now() + Duration::from_secs(60)));
assert_eq!(metrics.snapshot().timers_active(), 1);
drop(timer);
assert_eq!(metrics.snapshot().timers_active(), 0);
}
#[test]
fn remote_wake_unparks() {
let metrics = Metrics::default();
let config = Config {
metrics: metrics.clone(),
..Default::default()
};
let Some(mut worker) = worker_with(config) else { return };
let flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let thread_flag = flag.clone();
let waker_slot = std::sync::Arc::new(std::sync::Mutex::new(None::<std::task::Waker>));
let thread_slot = waker_slot.clone();
let thread = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(50));
thread_flag.store(true, Ordering::Release);
if let Some(waker) = thread_slot.lock().unwrap().take() {
waker.wake();
}
});
let start = Instant::now();
worker
.block_on(std::future::poll_fn(move |cx| {
if flag.load(Ordering::Acquire) {
return Poll::Ready(());
}
*waker_slot.lock().unwrap() = Some(cx.waker().clone());
Poll::Pending
}))
.unwrap();
assert!(start.elapsed() >= Duration::from_millis(50));
thread.join().unwrap();
assert!(metrics.snapshot().wakes > 0, "the remote wake went unreported");
}
}