Skip to main content

moq_uring/
worker.rs

1//! The per-thread worker: ring ownership, the drive loop, and parking.
2
3use std::cell::RefCell;
4use std::rc::{Rc, Weak};
5use std::sync::atomic::Ordering;
6use std::task::{Context, Poll};
7use std::time::{Duration, Instant};
8
9use io_uring::{EnterFlags, IoUring, opcode, types};
10
11use crate::metrics::Metrics;
12use crate::park::{FUTEX_BITSET_MATCH_ANY, FUTEX2_PRIVATE, FUTEX2_SIZE_U32, PARKED, RUNNING, Unpark};
13use crate::shared::{Cqe, Op, Shared, Task};
14use crate::{Error, timer, udp};
15
16/// Submission queue depth. The SQ only holds SQEs staged between submits,
17/// never in-flight operations, so it needs no relation to the socket pools;
18/// [`Shared::push`] submits inline whenever it fills.
19const SQ_ENTRIES: u32 = 256;
20
21/// Completion queue depth. Every in-flight operation can post a completion
22/// (one per send buffer with GSO on, one per provided receive buffer, the
23/// park futex, transient cancels), so this covers one socket at the default
24/// pool ceilings in [`udp::Config`], the one-socket-per-worker layout the
25/// relay runs. Running past it is not fatal: the kernel backlogs completions
26/// (`IORING_FEAT_NODROP`) rather than drop them. But the backlog is an
27/// allocation-per-CQE slow path and it ends any armed multishot receive, so
28/// the CQ is sized to keep it out of steady state.
29///
30/// No larger: the ring is charged to `RLIMIT_MEMLOCK` at 16 bytes per entry,
31/// most of each worker's footprint, and that budget is shared by every
32/// io_uring the user runs.
33const CQ_ENTRIES: u32 = 2048;
34
35/// Maximum completions copied at once while teardown is deadline-bounded.
36const TEARDOWN_CQE_BATCH: usize = 64;
37
38/// Extra mandatory submit attempts allowed after interrupted enters.
39const TEARDOWN_EINTR_RETRIES: usize = 8;
40
41/// Maximum time spent staging cancellations and draining completions.
42const TEARDOWN_TIMEOUT: Duration = Duration::from_millis(3200);
43
44/// Worker construction knobs.
45///
46/// The worker sizes its ring internally, with a completion queue that
47/// comfortably covers the per-socket pool ceilings in [`udp::Config`].
48#[derive(Debug, Default)]
49#[non_exhaustive]
50pub struct Config {
51	/// Where the worker accumulates its counters.
52	///
53	/// Default gives it a fresh set, still readable through
54	/// [`Handle::metrics`]. Pass one in to hold a copy on the thread that
55	/// spawned the worker, which is how an ops surface scrapes a worker it
56	/// cannot otherwise reach. A [`Metrics`] clone shares one set of counters,
57	/// so give each worker its own. Cloning this config starts a fresh set for
58	/// the cloned worker.
59	pub metrics: Metrics,
60}
61
62impl Clone for Config {
63	fn clone(&self) -> Self {
64		// A cloned construction plan targets a new worker, so its counters must
65		// not be folded into the source worker's per-worker series.
66		Self {
67			metrics: Metrics::default(),
68		}
69	}
70}
71
72/// A thread-pinned io_uring executor: the ring, a timer heap, and a local
73/// (`!Send`) task set, driven by a caller-owned loop.
74///
75/// Create one per thread, keep it on that thread (`!Send`), and drive it with
76/// [`block_on`](Self::block_on). Everything else reaches the worker through
77/// [`Handle`]: UDP sockets, timers, spawned tasks. Wakes from other threads
78/// (any `Waker` this worker minted) are an atomic store plus, only while the
79/// worker is parked, one futex syscall.
80///
81/// Dropping the worker makes a bounded attempt to submit the SQEs its last
82/// turn staged and drain their completions. A datagram already handed to a
83/// [`udp::Socket`] is included in that submission attempt, while operation
84/// storage that the kernel might still access is safely leaked if teardown
85/// cannot finish. It runs no tasks, though, so work a task has merely been
86/// asked for is not performed: a QUIC close is queued on its connection and
87/// framed by the driver task, so keep driving until the close is published
88/// rather than stopping the worker on the call that asked for it.
89pub struct Worker {
90	shared: Rc<Shared>,
91	tasks: kio::Tasks<Task>,
92	park: kio::Park,
93	/// Reused while copying CQEs out of the ring before dispatch.
94	cqes: Vec<Cqe>,
95	/// Whether the park-word `FUTEX_WAIT` SQE is in flight.
96	futex_armed: bool,
97}
98
99impl Worker {
100	/// Set up the ring, refusing kernels below Linux 6.12.
101	///
102	/// The floor buys incremental provided-buffer consumption, the absolute
103	/// park timeout, and batched minimum waits with one code path; there is
104	/// deliberately no fallback (use the tokio stack instead).
105	pub fn new(config: Config) -> Result<Self, Error> {
106		let Config { metrics } = config;
107		let metrics = metrics.counters().clone();
108		let ring = IoUring::builder()
109			.setup_single_issuer()
110			.setup_defer_taskrun()
111			.setup_coop_taskrun()
112			.setup_cqsize(CQ_ENTRIES)
113			.build(SQ_ENTRIES)
114			.map_err(|err| match err.raw_os_error() {
115				// EINVAL from setup means the kernel predates one of the
116				// requested flags (the ring geometry is compile-time valid),
117				// so it never reaches the feature check below.
118				Some(libc::ENOSYS) | Some(libc::EPERM) | Some(libc::EACCES) | Some(libc::EINVAL) => {
119					Error::Unsupported(format!(
120						"io_uring is unavailable ({err}); kernel {} (Linux 6.12+ required, and container seccomp \
121						 policies such as Docker's default commonly block io_uring)",
122						kernel_release()
123					))
124				}
125				_ => Error::ring(err),
126			})?;
127
128		// One feature bit gates the whole floor: MIN_TIMEOUT landed in 6.12
129		// alongside everything else this worker assumes.
130		if !ring.params().is_feature_min_timeout() {
131			return Err(Error::Unsupported(format!(
132				"kernel {} is too old: moq-uring requires Linux 6.12+ (io_uring MIN_TIMEOUT feature missing)",
133				kernel_release()
134			)));
135		}
136
137		Ok(Self {
138			shared: Rc::new(Shared {
139				ring: RefCell::new(ring),
140				ops: RefCell::new(slab::Slab::new()),
141				timers: Rc::new(RefCell::new(timer::Heap::new(metrics.clone()))),
142				spawns: RefCell::new(Vec::new()),
143				unpark: Unpark::new(metrics.clone()),
144				metrics,
145				next_bgid: std::cell::Cell::new(0),
146				stopped: std::cell::Cell::new(false),
147				spill: RefCell::new(std::collections::VecDeque::new()),
148			}),
149			tasks: kio::Tasks::new(),
150			park: kio::Park::default(),
151			cqes: Vec::new(),
152			futex_armed: false,
153		})
154	}
155
156	/// A cloneable handle for spawning, binding sockets, and minting timers.
157	pub fn handle(&self) -> Handle {
158		Handle {
159			shared: self.shared.clone(),
160		}
161	}
162
163	/// Drive the worker until `future` resolves.
164	///
165	/// Spawned tasks run alongside it and keep running across calls; they do
166	/// not keep `block_on` alive. An `Err` means the ring itself failed, which
167	/// is fatal to the worker.
168	pub fn block_on<F: Future>(&mut self, future: F) -> Result<F::Output, Error> {
169		let mut future = std::pin::pin!(future);
170		let waker = self.shared.unpark.waker();
171		loop {
172			// Adopt tasks spawned since the last turn (spawning wakes us).
173			let spawns = std::mem::take(&mut *self.shared.spawns.borrow_mut());
174			for task in spawns {
175				self.tasks.push(task);
176			}
177
178			let cx = Context::from_waker(&waker);
179			let waiter = self.park.hold(&cx);
180			if let Poll::Ready(value) = waiter.poll_future(future.as_mut()) {
181				return Ok(value);
182			}
183			// `Ready` just means the set is drained; the waiter stays
184			// registered for the next push.
185			let _ = self.tasks.poll(waiter);
186
187			self.shared.timers.borrow_mut().fire(Instant::now());
188			self.pump()?;
189			self.maybe_park()?;
190		}
191	}
192
193	/// Submit staged SQEs and dispatch every pending completion.
194	fn pump(&mut self) -> Result<(), Error> {
195		self.pump_inner(None)
196	}
197
198	/// Pump submission and completion batches until `deadline`.
199	fn pump_until(&mut self, deadline: Instant) -> Result<(), Error> {
200		self.pump_inner(Some(deadline))
201	}
202
203	fn pump_inner(&mut self, deadline: Option<Instant>) -> Result<(), Error> {
204		self.submit()?;
205		loop {
206			if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
207				return Ok(());
208			}
209			// Copy the completions out so dispatch can borrow the ring (to
210			// re-arm receives, push cancels, and so on). Completions spilled
211			// by `Shared::push` predate the CQ's, so they dispatch first.
212			self.cqes.clear();
213			{
214				let mut ring = self.shared.ring.borrow_mut();
215				let mut spill = self.shared.spill.borrow_mut();
216				let limit = deadline.map_or(usize::MAX, |_| TEARDOWN_CQE_BATCH);
217				let spilled = spill.len().min(limit);
218				self.cqes.extend(spill.drain(..spilled));
219				self.cqes
220					.extend(ring.completion().take(limit - spilled).map(|entry| Cqe {
221						user_data: entry.user_data(),
222						result: entry.result(),
223						flags: entry.flags(),
224					}));
225			}
226			self.shared.metrics.completions.add(self.cqes.len() as u64);
227			if self.cqes.is_empty() || !self.dispatch_batch(deadline, Instant::now) {
228				return Ok(());
229			}
230		}
231	}
232
233	/// Dispatch the collected batch in `self.cqes` while its teardown budget
234	/// remains.
235	fn dispatch_batch(&mut self, deadline: Option<Instant>, mut now: impl FnMut() -> Instant) -> bool {
236		for index in 0..self.cqes.len() {
237			if deadline.is_some_and(|deadline| now() >= deadline) {
238				// Drop this batch's remaining CQEs. Worker::drop will leak their
239				// op state, which is safe even if the kernel already finished it.
240				return false;
241			}
242			let cqe = self.cqes[index];
243			self.dispatch(cqe);
244		}
245		true
246	}
247
248	fn submit(&mut self) -> Result<(), Error> {
249		let mut ring = self.shared.ring.borrow_mut();
250		if ring.submission().is_empty() {
251			return Ok(());
252		}
253		self.shared.metrics.enters.add(1);
254		match ring.submit() {
255			// A partial submit leaves the rest staged for the next pump.
256			Ok(count) => {
257				self.shared.metrics.submissions.add(count as u64);
258				Ok(())
259			}
260			// A signal interrupted the enter before it consumed anything. The
261			// next worker turn retries the same staged SQEs.
262			Err(err) if err.raw_os_error() == Some(libc::EINTR) => Ok(()),
263			// The completion queue overflowed; the caller reaps and retries.
264			Err(err) if err.raw_os_error() == Some(libc::EBUSY) => Ok(()),
265			Err(err) => Err(err.into()),
266		}
267	}
268
269	/// Submit every residual SQE without waiting for completions.
270	fn submit_teardown(&mut self) -> Result<(), Error> {
271		let mut ring = self.shared.ring.borrow_mut();
272		let mut interruptions = 0;
273		loop {
274			if ring.submission().is_empty() {
275				return Ok(());
276			}
277			self.shared.metrics.enters.add(1);
278			match ring.submit() {
279				// Keep submitting after partial progress. Returning zero while SQEs
280				// remain would otherwise spin forever.
281				Ok(0) => {
282					return Err(std::io::Error::other("io_uring teardown submission made no progress").into());
283				}
284				Ok(count) => self.shared.metrics.submissions.add(count as u64),
285				Err(err) => retry_teardown_submit(&mut interruptions, err)?,
286			}
287		}
288	}
289
290	/// Submit residual SQEs, then drain completions within `deadline`.
291	fn drain_teardown(&mut self, deadline: Instant) {
292		// Cancellation staging can consume the whole deadline. Existing SQEs,
293		// especially sends, must still reach the kernel before it gates draining.
294		let submission_failed = self.submit_teardown().is_err();
295		if !submission_failed {
296			loop {
297				if self.shared.ops.borrow().is_empty() {
298					return;
299				}
300				if Instant::now() >= deadline || self.pump_until(deadline).is_err() {
301					break;
302				}
303				let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
304					break;
305				};
306				let ring = self.shared.ring.borrow_mut();
307				let wait = remaining.min(std::time::Duration::from_millis(50));
308				let ts = types::Timespec::from(wait);
309				let args = types::SubmitArgs::new().timespec(&ts);
310				self.shared.metrics.enters.add(1);
311				let _ = ring.submitter().submit_with_args(1, &args);
312			}
313		}
314		if !self.shared.ops.borrow().is_empty() {
315			// Leak the operations (and what they own) rather than free memory
316			// the kernel may still touch.
317			tracing::error!("dropping an io_uring worker with operations stuck in flight; leaking them");
318			std::mem::forget(std::mem::take(&mut *self.shared.ops.borrow_mut()));
319		}
320	}
321
322	/// Route one completion to its operation.
323	fn dispatch(&mut self, cqe: Cqe) {
324		let key = cqe.user_data as usize;
325
326		// Terminal completions take their op out of the slab, releasing what
327		// the kernel is now done with. A multishot receive with `more` set
328		// stays armed, so only its socket is borrowed. The kernel posts
329		// nothing for a key after its terminal CQE, so reusing the slot for
330		// an op armed during dispatch is sound.
331		enum Route {
332			Live(Rc<udp::SockShared>),
333			Done(Op),
334		}
335
336		let route = {
337			let mut ops = self.shared.ops.borrow_mut();
338			let Some(op) = ops.get(key) else {
339				tracing::error!(key, "completion for an unknown operation");
340				return;
341			};
342			let terminal = match op {
343				Op::Recv { .. } => cqe.result < 0 || !io_uring::cqueue::more(cqe.flags),
344				_ => true,
345			};
346			if terminal {
347				Route::Done(ops.remove(key))
348			} else {
349				match op {
350					Op::Recv { sock, .. } => Route::Live(sock.clone()),
351					_ => unreachable!("only receives are non-terminal"),
352				}
353			}
354		};
355
356		match route {
357			Route::Live(sock) => udp::on_recv(&self.shared, &sock, None, cqe, false),
358			Route::Done(Op::Recv { sock, one }) => udp::on_recv(&self.shared, &sock, one, cqe, true),
359			Route::Done(Op::Send(op)) => udp::on_send(op, cqe),
360			Route::Done(Op::FutexWait) => self.futex_armed = false,
361			Route::Done(Op::Cancel) => {}
362		}
363	}
364
365	/// Park in `io_uring_enter` until a completion, a timer deadline, or a
366	/// remote wake, unless a wake already arrived.
367	fn maybe_park(&mut self) -> Result<(), Error> {
368		let unpark = self.shared.unpark.clone();
369		if unpark
370			.word
371			.compare_exchange(RUNNING, PARKED, Ordering::AcqRel, Ordering::Acquire)
372			.is_err()
373		{
374			// Notified: consume it and poll again instead of parking.
375			unpark.word.store(RUNNING, Ordering::Release);
376			return Ok(());
377		}
378
379		// Keep exactly one FUTEX_WAIT armed. It waits while the word still
380		// holds PARKED; a remote unpark stores NOTIFIED and kicks the futex,
381		// and if the store lands before this submission the wait completes
382		// immediately with EAGAIN. Either way there is a CQE to wake us.
383		if !self.futex_armed {
384			let key = self.shared.insert(Op::FutexWait);
385			let entry = opcode::FutexWait::new(
386				unpark.word.as_ptr(),
387				PARKED as u64,
388				FUTEX_BITSET_MATCH_ANY,
389				FUTEX2_SIZE_U32 | FUTEX2_PRIVATE,
390			)
391			.build()
392			.user_data(key);
393			if let Err(err) = self.shared.push(&entry) {
394				self.shared.ops.borrow_mut().remove(key as usize);
395				unpark.word.store(RUNNING, Ordering::Release);
396				return Err(err.into());
397			}
398			self.futex_armed = true;
399		}
400
401		let deadline = self.shared.timers.borrow().next();
402		self.shared.metrics.parks.add(1);
403		self.shared.metrics.enters.add(1);
404		let result = {
405			let mut ring = self.shared.ring.borrow_mut();
406			let to_submit = ring.submission().len() as u32;
407			let submitter = ring.submitter();
408			match deadline {
409				None => submitter.submit_and_wait(1),
410				Some(at) => {
411					// Zero timeout SQEs: the earliest userspace deadline rides
412					// the enter call as an absolute CLOCK_MONOTONIC timeout.
413					let ts = abs_timespec(at);
414					let args = types::SubmitArgs::new().timespec(&ts);
415					let flags = EnterFlags::GETEVENTS | EnterFlags::EXT_ARG | EnterFlags::ABS_TIMER;
416					// SAFETY: `args` (and the timespec it references) outlive
417					// the call, and EXT_ARG matches its type.
418					unsafe { submitter.enter(to_submit, 1, flags.bits(), Some(&args)) }
419				}
420			}
421		};
422		unpark.word.store(RUNNING, Ordering::Release);
423
424		match result {
425			Ok(count) => {
426				self.shared.metrics.submissions.add(count as u64);
427				Ok(())
428			}
429			Err(err)
430				if matches!(
431					err.raw_os_error(),
432					Some(libc::ETIME) | Some(libc::EINTR) | Some(libc::EBUSY)
433				) =>
434			{
435				Ok(())
436			}
437			Err(err) => Err(err.into()),
438		}
439	}
440}
441
442impl Drop for Worker {
443	fn drop(&mut self) {
444		// Handles may outlive us; everything they try from here on fails
445		// instead of pending on a loop that will never run again.
446		self.shared.stopped.set(true);
447		// One deadline bounds cancellation staging and draining together.
448		let deadline = Instant::now() + TEARDOWN_TIMEOUT;
449		// The kernel may still write into provided buffers and read send
450		// headers owned by the ops slab. Queue cancels behind every staged
451		// receive and the futex, so partial submissions cannot strand an
452		// uncancelled operation. Sends are deliberately left alone: a datagram
453		// staged by the final worker turn still has to reach the wire.
454		let cancel: Vec<u64> = self
455			.shared
456			.ops
457			.borrow()
458			.iter()
459			.filter_map(|(key, op)| matches!(op, Op::Recv { .. } | Op::FutexWait).then_some(key as u64))
460			.collect();
461		let mut cancellation_failed = false;
462		for key in cancel {
463			if Instant::now() >= deadline {
464				cancellation_failed = true;
465				break;
466			}
467			cancellation_failed |= self.shared.cancel_until(key, deadline).is_err();
468		}
469		if cancellation_failed {
470			tracing::error!("failed to queue one or more io_uring teardown cancellations");
471		}
472		self.drain_teardown(deadline);
473	}
474}
475
476impl std::fmt::Debug for Worker {
477	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478		f.debug_struct("Worker").field("tasks", &self.tasks.len()).finish()
479	}
480}
481
482/// A worker's cloneable, thread-local handle.
483///
484/// Everything that is not the drive loop goes through this:
485/// [`spawn`](Self::spawn), [`udp`](Self::udp), [`timer`](Self::timer), and
486/// [`run`](Self::run) for MoQ drivers. `!Send`, like everything the worker owns.
487pub struct Handle {
488	shared: Rc<Shared>,
489}
490
491impl Clone for Handle {
492	fn clone(&self) -> Self {
493		Self {
494			shared: self.shared.clone(),
495		}
496	}
497}
498
499impl Handle {
500	/// This worker's counters, readable from any thread.
501	pub fn metrics(&self) -> Metrics {
502		Metrics::from_counters(self.shared.metrics.clone())
503	}
504
505	/// Run a `!Send` future on this worker until completion.
506	///
507	/// If the worker has already been dropped the future is dropped instead of
508	/// running, like a task spawned on a shut-down runtime.
509	pub fn spawn(&self, future: impl Future<Output = ()> + 'static) {
510		if self.shared.stopped.get() {
511			return;
512		}
513		let mut future = Box::pin(future);
514		self.shared
515			.spawns
516			.borrow_mut()
517			.push(Box::new(move |waiter: &kio::Waiter| {
518				waiter.poll_future(future.as_mut())
519			}));
520		// Spawning from another task (or before block_on) must reach the next
521		// turn's drain.
522		self.shared.unpark.unpark();
523	}
524
525	/// Drive `socket` through this worker's ring.
526	///
527	/// The caller configures and binds the socket (options, addresses); this
528	/// takes over receive and send. `config` picks the batching mechanisms.
529	///
530	/// The socket is what names this worker from here on: an
531	/// [`Endpoint`](crate::quic::Endpoint) built on it runs its tasks here,
532	/// whichever thread's handle built it. A member of a steered reuseport
533	/// group ([`moq_sock::shard::Socket`]) brings its slot along, so the
534	/// connection ids issued through it steer back to this socket.
535	pub fn udp(&self, socket: impl Into<udp::Bound>, config: udp::Config) -> Result<udp::Socket, Error> {
536		if self.shared.stopped.get() {
537			return Err(Shared::gone_error().into());
538		}
539		udp::Socket::bind(&self.shared, socket.into(), config)
540	}
541}
542
543/// The worker behind a socket, endpoint, or connection, held weakly.
544///
545/// I/O carries its owner so it cannot be driven through a different worker,
546/// but a handle to that I/O must not keep a dropped worker's ring alive, so
547/// this holds no strong reference. Once the worker is gone, spawning is a
548/// no-op (like [`Handle::spawn`]) and timers never fire, which is what the
549/// tasks that would have consumed them expect.
550#[derive(Clone)]
551pub(crate) struct Owner {
552	shared: Weak<Shared>,
553	/// Held directly: a timer on a dropped worker still has to exist, since
554	/// the driver that owns it is torn down by the same drop that would need
555	/// it.
556	timers: Rc<RefCell<timer::Heap>>,
557}
558
559impl Owner {
560	pub(crate) fn new(shared: &Rc<Shared>) -> Self {
561		Self {
562			shared: Rc::downgrade(shared),
563			timers: shared.timers.clone(),
564		}
565	}
566
567	/// The worker's core while it is still allocated, torn down or not.
568	pub fn upgrade(&self) -> Option<Rc<Shared>> {
569		self.shared.upgrade()
570	}
571
572	/// A strong handle, or `None` once the worker is dropped or torn down.
573	pub fn handle(&self) -> Option<Handle> {
574		let shared = self.shared.upgrade()?;
575		(!shared.stopped.get()).then_some(Handle { shared })
576	}
577
578	/// Run a `!Send` future on the worker, or drop it if the worker is gone.
579	pub fn spawn(&self, future: impl Future<Output = ()> + 'static) {
580		if let Some(handle) = self.handle() {
581			handle.spawn(future);
582		}
583	}
584
585	/// A disarmed timer on the worker.
586	pub fn timer(&self) -> crate::Timer {
587		crate::Timer::from_heap(self.timers.clone())
588	}
589
590	/// A timer that expires after `duration`.
591	pub fn after(&self, duration: Duration) -> crate::Timer {
592		let mut timer = self.timer();
593		timer.set(Instant::now().checked_add(duration));
594		timer
595	}
596}
597
598impl std::fmt::Debug for Handle {
599	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600		f.debug_struct("Handle").finish()
601	}
602}
603
604impl Handle {
605	/// Allocate a disarmed timer on this worker.
606	pub fn timer(&self) -> crate::Timer {
607		crate::Timer::from_heap(self.shared.timers.clone())
608	}
609
610	/// Run a MoQ driver with this worker's timer and monotonic clock, resolving
611	/// with its terminal error.
612	pub async fn run<D: moq_net::time::Driver>(&self, mut driver: D) -> moq_net::Error {
613		let mut timer = self.timer();
614		kio::wait(|waiter| {
615			loop {
616				match driver.poll(Instant::now(), waiter) {
617					Ok(at) => timer.set(at),
618					Err(err) => return Poll::Ready(err),
619				}
620				if timer.poll(waiter).is_pending() {
621					return Poll::Pending;
622				}
623			}
624		})
625		.await
626	}
627}
628
629/// The running kernel release, for error messages.
630fn kernel_release() -> String {
631	// SAFETY: all-zero is a valid utsname out-buffer.
632	let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
633	// SAFETY: valid out-pointer.
634	if unsafe { libc::uname(&mut uts) } != 0 {
635		return "unknown".into();
636	}
637	// SAFETY: uname NUL-terminates the release field.
638	unsafe { std::ffi::CStr::from_ptr(uts.release.as_ptr()) }
639		.to_string_lossy()
640		.into_owned()
641}
642
643/// Convert a deadline into an absolute `CLOCK_MONOTONIC` timespec (what
644/// `IORING_ENTER_ABS_TIMER` expects).
645fn abs_timespec(at: Instant) -> types::Timespec {
646	// `std::time::Instant` is CLOCK_MONOTONIC on Linux but its origin is
647	// opaque, so anchor the difference on a raw clock read.
648	let delta = at.saturating_duration_since(Instant::now());
649	let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
650	// SAFETY: valid out-pointer.
651	unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) };
652	let nanos = now.tv_nsec as u64 + delta.subsec_nanos() as u64;
653	let secs = (now.tv_sec as u64)
654		.saturating_add(delta.as_secs())
655		.saturating_add(nanos / 1_000_000_000);
656	types::Timespec::new().sec(secs).nsec((nanos % 1_000_000_000) as u32)
657}
658
659/// Accept a bounded number of interrupted teardown submissions.
660fn retry_teardown_submit(interruptions: &mut usize, err: std::io::Error) -> std::io::Result<()> {
661	if err.raw_os_error() != Some(libc::EINTR) || *interruptions >= TEARDOWN_EINTR_RETRIES {
662		return Err(err);
663	}
664	*interruptions += 1;
665	Ok(())
666}
667
668#[cfg(test)]
669mod tests {
670	use super::*;
671
672	use crate::Timer as Deadline;
673	use std::time::Duration;
674
675	/// Kernel-gated: `None` (with a loud skip) below the 6.12 floor, so these
676	/// tests pass vacuously on older CI kernels and run everywhere else.
677	fn worker() -> Option<Worker> {
678		worker_with(Config::default())
679	}
680
681	fn worker_with(config: Config) -> Option<Worker> {
682		match Worker::new(config) {
683			Ok(worker) => Some(worker),
684			Err(Error::Unsupported(reason)) => {
685				eprintln!("skipping io_uring test: {reason}");
686				None
687			}
688			Err(err) => panic!("worker setup failed: {err}"),
689		}
690	}
691
692	#[test]
693	fn cloned_config_has_fresh_metrics() {
694		let config = Config::default();
695		let clone = config.clone();
696		assert!(!std::sync::Arc::ptr_eq(
697			config.metrics.counters(),
698			clone.metrics.counters()
699		));
700	}
701
702	#[test]
703	fn ready_future() {
704		let Some(mut worker) = worker() else { return };
705		let value = worker.block_on(async { 7 }).unwrap();
706		assert_eq!(value, 7);
707	}
708
709	#[test]
710	fn spawned_tasks_run() {
711		let Some(mut worker) = worker() else { return };
712		let handle = worker.handle();
713		let flag = Rc::new(std::cell::Cell::new(0));
714
715		for index in 0..3 {
716			let flag = flag.clone();
717			handle.spawn(async move {
718				flag.set(flag.get() + index + 1);
719			});
720		}
721		// Spawned tasks run even while the main future pends on a timer.
722		let handle2 = handle.clone();
723		worker
724			.block_on(async move {
725				Deadline::after(&handle2, Duration::from_millis(10)).wait().await;
726			})
727			.unwrap();
728		assert_eq!(flag.get(), 6);
729	}
730
731	#[test]
732	fn deadline_fires_at_park() {
733		let Some(mut worker) = worker() else { return };
734		let handle = worker.handle();
735		let start = Instant::now();
736		// Nothing else wakes this worker: the park's absolute timeout is the
737		// only thing that can fire the deadline.
738		worker
739			.block_on(async move {
740				Deadline::after(&handle, Duration::from_millis(50)).wait().await;
741			})
742			.unwrap();
743		let elapsed = start.elapsed();
744		assert!(elapsed >= Duration::from_millis(50), "woke early: {elapsed:?}");
745		assert!(elapsed < Duration::from_secs(5), "woke far too late: {elapsed:?}");
746	}
747
748	#[test]
749	fn timer_rearm_and_disarm() {
750		let Some(mut worker) = worker() else { return };
751		let handle = worker.handle();
752		let mut timer = handle.timer();
753
754		// Disarmed timers never fire.
755		assert!(timer.poll(&kio::Waiter::noop()).is_pending());
756
757		// An instant already in the past is immediately elapsed, and stays
758		// elapsed (fused) until re-armed.
759		timer.set(Some(Instant::now() - Duration::from_millis(1)));
760		assert!(timer.poll(&kio::Waiter::noop()).is_ready());
761		assert!(timer.poll(&kio::Waiter::noop()).is_ready());
762
763		// Re-arming to the future pends again; disarming stays pending.
764		timer.set(Some(Instant::now() + Duration::from_secs(60)));
765		assert!(timer.poll(&kio::Waiter::noop()).is_pending());
766		timer.set(None);
767		assert!(timer.poll(&kio::Waiter::noop()).is_pending());
768
769		// And a short re-arm actually fires through the worker.
770		let start = Instant::now();
771		worker
772			.block_on(async move {
773				timer.set(Some(Instant::now() + Duration::from_millis(20)));
774				kio::wait(|waiter| timer.poll(waiter)).await;
775			})
776			.unwrap();
777		assert!(start.elapsed() >= Duration::from_millis(20));
778	}
779
780	#[test]
781	fn dropped_worker_rejects_operations() {
782		let Some(worker) = worker() else { return };
783		let handle = worker.handle();
784		let bind = || std::net::UdpSocket::bind("127.0.0.1:0").expect("bind");
785		let sock = handle.udp(bind(), udp::Config::default()).expect("socket");
786		let shared = sock.downgrade();
787		let to = sock.local_addr().expect("addr");
788		let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
789			panic!("no tx buffer");
790		};
791		drop(worker);
792
793		// Every path a retained handle can reach fails instead of pending on
794		// a loop that will never run again.
795		assert!(handle.udp(bind(), udp::Config::default()).is_err());
796		assert!(matches!(sock.poll_recv(&kio::Waiter::noop()), Poll::Ready(Err(_))));
797		assert!(matches!(sock.poll_acquire(&kio::Waiter::noop()), Poll::Ready(Err(_))));
798		assert!(
799			tx.send(udp::Transmit {
800				to,
801				len: 1200,
802				segment: 1200,
803				ecn: None,
804			})
805			.is_err()
806		);
807		// And a late spawn is dropped rather than parked forever.
808		handle.spawn(async {});
809		drop(sock);
810		assert!(shared.upgrade().is_none(), "the worker leaked its staged receive");
811	}
812
813	#[test]
814	fn teardown_stops_between_completions_at_the_deadline() {
815		let Some(mut worker) = worker() else { return };
816		let first = worker.shared.insert(Op::Cancel);
817		let second = worker.shared.insert(Op::Cancel);
818		let cqe = |user_data| Cqe {
819			user_data,
820			result: 0,
821			flags: 0,
822		};
823		let before = Instant::now();
824		let deadline = before + Duration::from_millis(1);
825		let mut now = [before, deadline].into_iter();
826
827		worker.cqes = vec![cqe(first), cqe(second)];
828		assert!(!worker.dispatch_batch(Some(deadline), || {
829			now.next().expect("one deadline check per completion")
830		}));
831		assert!(!worker.shared.ops.borrow().contains(first as usize));
832		assert!(worker.shared.ops.borrow().contains(second as usize));
833		worker.shared.ops.borrow_mut().remove(second as usize);
834	}
835
836	#[test]
837	fn expired_teardown_submits_residual_sqes() {
838		let Some(mut worker) = worker() else { return };
839		// A NOP needs no slab-owned memory, so it can observe the SQ directly.
840		for _ in 0..SQ_ENTRIES {
841			worker.shared.push(&opcode::Nop::new().build()).expect("stage NOP");
842		}
843		assert_eq!(worker.shared.ring.borrow_mut().submission().len(), SQ_ENTRIES as usize);
844
845		worker.drain_teardown(Instant::now());
846		assert!(worker.shared.ring.borrow_mut().submission().is_empty());
847	}
848
849	#[test]
850	fn teardown_submit_interrupt_budget_is_finite() {
851		let interrupted = || std::io::Error::from_raw_os_error(libc::EINTR);
852		let mut interruptions = 0;
853		for _ in 0..TEARDOWN_EINTR_RETRIES {
854			retry_teardown_submit(&mut interruptions, interrupted()).expect("retry interrupted submit");
855		}
856		assert_eq!(interruptions, TEARDOWN_EINTR_RETRIES);
857		assert_eq!(
858			retry_teardown_submit(&mut interruptions, interrupted())
859				.expect_err("interrupt budget must be finite")
860				.raw_os_error(),
861			Some(libc::EINTR)
862		);
863	}
864
865	#[test]
866	fn dropped_worker_drains_more_receives_than_the_submission_queue() {
867		let Some(worker) = worker() else { return };
868		let handle = worker.handle();
869		let config = udp::Config {
870			gro: false,
871			gso: false,
872			multishot: false,
873			rx_buffers_max: 1,
874			rx_buffer_len: 2048,
875			tx_buffers_max: 1,
876			tx_buffer_len: 2048,
877		};
878		let mut sockets = Vec::new();
879		let mut shared = Vec::new();
880		for _ in 0..=SQ_ENTRIES {
881			let sock = handle
882				.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config.clone())
883				.expect("socket");
884			shared.push(sock.downgrade());
885			sockets.push(sock);
886		}
887
888		drop(worker);
889		drop(sockets);
890		assert!(
891			shared.iter().all(|shared| shared.upgrade().is_none()),
892			"the worker leaked a receive staged across submission batches"
893		);
894	}
895
896	#[test]
897	fn cq_covers_the_default_pool_ceilings() {
898		// The completion queue must cover a socket at its default pool
899		// ceilings (plus the futex), or the kernel's overflow slow path
900		// becomes steady state for the workload the ceilings exist to serve.
901		// Fails when someone raises the udp defaults without revisiting
902		// CQ_ENTRIES.
903		let config = udp::Config::default();
904		let per_socket = u32::from(config.tx_buffers_max) + u32::from(config.rx_buffers_max);
905		assert!(CQ_ENTRIES > per_socket, "CQ_ENTRIES fell behind the pool defaults");
906	}
907
908	#[test]
909	fn the_ring_honors_the_requested_cq_depth() {
910		// The kernel-reported geometry, not the constant: dropping the
911		// `setup_cqsize` call would silently fall back to a CQ of twice the SQ
912		// (512), and the overflow test below cannot catch that because it
913		// expects overflow. This one pins the operative fix.
914		let Some(worker) = worker() else { return };
915		let cq = worker.shared.ring.borrow().params().cq_entries();
916		assert!(cq >= CQ_ENTRIES, "kernel granted a {cq}-entry CQ, wanted {CQ_ENTRIES}");
917	}
918
919	#[test]
920	fn completion_overflow_is_survivable() {
921		let Some(mut worker) = worker() else { return };
922		let handle = worker.handle();
923		// Twice the CQ's worth of sends, staged synchronously so nothing
924		// reaps while they complete: the kernel must backlog the completions
925		// (`IORING_FEAT_NODROP`) and the worker must drain them without any
926		// operation, socket, or the worker itself failing.
927		let ceiling = (CQ_ENTRIES * 2) as u16;
928		let config = udp::Config {
929			tx_buffers_max: ceiling,
930			tx_buffer_len: 2048,
931			..Default::default()
932		};
933		let sock = handle
934			.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
935			.expect("socket");
936		let to = sock.local_addr().expect("addr");
937
938		let mut held = Vec::new();
939		while let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) {
940			held.push(tx);
941		}
942		assert_eq!(held.len(), usize::from(ceiling));
943		for tx in held.drain(..) {
944			tx.send(udp::Transmit {
945				to,
946				len: 1200,
947				segment: 1200,
948				ecn: None,
949			})
950			.expect("send");
951		}
952
953		// The point of the test is the overflow, so prove it happened: the
954		// kernel raises this flag while completions sit in its backlog. It
955		// clears once the backlog flushes, so sample it before each sweep.
956		let saw_overflow = |worker: &Worker| worker.shared.ring.borrow_mut().submission().cq_overflow();
957		let mut overflowed = saw_overflow(&worker);
958
959		// Drive the worker until every completion, backlog included, has been
960		// reaped and released its buffer back to the pool.
961		let deadline = Instant::now() + Duration::from_secs(10);
962		loop {
963			overflowed = overflowed || saw_overflow(&worker);
964			let h = handle.clone();
965			worker
966				.block_on(async move {
967					Deadline::after(&h, Duration::from_millis(10)).wait().await;
968				})
969				.unwrap();
970			let mut free = Vec::new();
971			loop {
972				match sock.poll_acquire(&kio::Waiter::noop()) {
973					Poll::Ready(Ok(tx)) => free.push(tx),
974					Poll::Ready(Err(err)) => panic!("send path failed: {err}"),
975					Poll::Pending => break,
976				}
977			}
978			if free.len() == usize::from(ceiling) {
979				break;
980			}
981			assert!(
982				Instant::now() < deadline,
983				"buffers stuck in flight: {} of {ceiling} free",
984				free.len()
985			);
986		}
987		assert!(overflowed, "the burst never overflowed the CQ; it proves nothing");
988		// The receive side rode out the same storm: whatever the loopback
989		// delivered drains without a terminal error.
990		while let Poll::Ready(result) = sock.poll_recv(&kio::Waiter::noop()) {
991			result.expect("receive path failed");
992		}
993	}
994
995	#[test]
996	fn oversized_receive_pool_is_rejected() {
997		let Some(worker) = worker() else { return };
998		let handle = worker.handle();
999		// Without validation the power-of-two rounding wraps to a zero-entry
1000		// ring, which allocates nothing and underflows its mask.
1001		let config = udp::Config {
1002			rx_buffers_max: u16::MAX,
1003			..Default::default()
1004		};
1005		let err = handle
1006			.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
1007			.expect_err("oversized pool");
1008		assert!(matches!(err, Error::Io(err) if err.kind() == std::io::ErrorKind::InvalidInput));
1009	}
1010
1011	#[test]
1012	fn the_send_pool_grows_to_its_ceiling() {
1013		let Some(worker) = worker() else { return };
1014		let handle = worker.handle();
1015		// A ceiling is a bound, not a reservation: 65535 default-length buffers
1016		// would be 4 GiB if the pool were allocated up front.
1017		let config = udp::Config {
1018			tx_buffers_max: u16::MAX,
1019			..Default::default()
1020		};
1021		handle
1022			.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
1023			.expect("socket");
1024
1025		// Short buffers so the whole ceiling fits in a test.
1026		let config = udp::Config {
1027			tx_buffers_max: 200,
1028			tx_buffer_len: 4096,
1029			..Default::default()
1030		};
1031		let sock = handle
1032			.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
1033			.expect("socket");
1034
1035		// Holding every buffer starves the pool, which grows past its initial
1036		// floor rather than serializing the caller behind it, and stops at the
1037		// ceiling.
1038		let mut held = Vec::new();
1039		while let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) {
1040			held.push(tx);
1041		}
1042		assert_eq!(held.len(), 200);
1043		drop(worker);
1044	}
1045
1046	#[test]
1047	fn ungso_send_is_not_capped_at_a_train() {
1048		let Some(worker) = worker() else { return };
1049		let handle = worker.handle();
1050		// Without GSO each segment rides its own `sendmsg`, so the kernel's
1051		// 64-segment train limit does not apply.
1052		let config = udp::Config {
1053			gso: false,
1054			..Default::default()
1055		};
1056		let sock = handle
1057			.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
1058			.expect("socket");
1059		let to = sock.local_addr().expect("addr");
1060		let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
1061			panic!("no tx buffer");
1062		};
1063		tx.send(udp::Transmit {
1064			to,
1065			len: 64 * 1024,
1066			segment: 1000,
1067			ecn: None,
1068		})
1069		.expect("send 66 datagrams");
1070		drop(worker);
1071	}
1072
1073	#[test]
1074	fn ungso_send_is_capped_by_the_ring() {
1075		let Some(worker) = worker() else { return };
1076		let handle = worker.handle();
1077		// Without GSO the segment count is the `sendmsg` count, and `push`
1078		// submits inline without reaping once the queue is full, so one call
1079		// must not outrun the ring.
1080		let config = udp::Config {
1081			gso: false,
1082			..Default::default()
1083		};
1084		let sock = handle
1085			.udp(std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"), config)
1086			.expect("socket");
1087		let to = sock.local_addr().expect("addr");
1088		let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
1089			panic!("no tx buffer");
1090		};
1091		let err = tx
1092			.send(udp::Transmit {
1093				to,
1094				len: 64 * 1024,
1095				segment: 1,
1096				ecn: None,
1097			})
1098			.expect_err("65536 datagrams from one buffer");
1099		assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1100		drop(worker);
1101	}
1102
1103	#[test]
1104	fn oversized_gso_segment_is_rejected() {
1105		let Some(worker) = worker() else { return };
1106		let handle = worker.handle();
1107		let sock = handle
1108			.udp(
1109				std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"),
1110				udp::Config::default(),
1111			)
1112			.expect("socket");
1113		let to = sock.local_addr().expect("addr");
1114		let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
1115			panic!("no tx buffer");
1116		};
1117		// `UDP_SEGMENT` is a u16: without validation this would truncate to a
1118		// one-byte stride instead of one segment.
1119		let err = tx
1120			.send(udp::Transmit {
1121				to,
1122				len: 60_000,
1123				segment: usize::from(u16::MAX) + 2,
1124				ecn: None,
1125			})
1126			.expect_err("oversized segment");
1127		assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
1128		drop(worker);
1129	}
1130
1131	/// The counters an ops scrape reads have to move for real work, and a
1132	/// handed-in [`Metrics`] has to be the same set the worker writes: reading
1133	/// zeros off a worker that is busy is indistinguishable from a healthy idle
1134	/// one, which is the failure this whole surface exists to prevent.
1135	#[test]
1136	fn metrics_record_ring_and_socket_activity() {
1137		let metrics = Metrics::default();
1138		let config = Config {
1139			metrics: metrics.clone(),
1140			..Default::default()
1141		};
1142		let Some(mut worker) = worker_with(config) else { return };
1143		let handle = worker.handle();
1144		let sock = handle
1145			.udp(
1146				std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"),
1147				udp::Config::default(),
1148			)
1149			.expect("socket");
1150		let to = sock.local_addr().expect("addr");
1151
1152		// One GSO train of four datagrams: one `sendmsg`, four packets.
1153		let Poll::Ready(Ok(mut tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
1154			panic!("no tx buffer");
1155		};
1156		tx[..4 * 1200].fill(7);
1157		tx.send(udp::Transmit {
1158			to,
1159			len: 4 * 1200,
1160			segment: 1200,
1161			ecn: None,
1162		})
1163		.expect("send");
1164
1165		// Drive the worker until the loopback delivers, parking on a timer each
1166		// turn so the park and timer counters see traffic too.
1167		let deadline = Instant::now() + Duration::from_secs(5);
1168		let mut received = 0;
1169		while received == 0 && Instant::now() < deadline {
1170			let handle = handle.clone();
1171			worker
1172				.block_on(async move {
1173					Deadline::after(&handle, Duration::from_millis(10)).wait().await;
1174				})
1175				.unwrap();
1176			while let Poll::Ready(packet) = sock.poll_recv(&kio::Waiter::noop()) {
1177				let packet = packet.expect("receive path failed");
1178				received += packet.payload().len();
1179			}
1180		}
1181		assert!(received > 0, "the loopback never delivered the send");
1182
1183		let snap = metrics.snapshot();
1184		assert_eq!(snap.tx_sends, 1, "one GSO train is one sendmsg: {snap:?}");
1185		assert_eq!(snap.tx_datagrams, 4, "four segments: {snap:?}");
1186		assert!(snap.rx_receives > 0, "no receive completions: {snap:?}");
1187		assert!(
1188			snap.rx_datagrams >= snap.rx_receives,
1189			"fewer datagrams than receives: {snap:?}"
1190		);
1191		assert!(snap.submissions > 0, "nothing was submitted: {snap:?}");
1192		assert!(snap.completions > 0, "nothing completed: {snap:?}");
1193		assert!(snap.enters > 0, "the ring was never entered: {snap:?}");
1194		assert!(snap.parks > 0, "the worker never parked: {snap:?}");
1195		assert!(snap.timers_fired > 0, "the park deadlines never fired: {snap:?}");
1196		// The worker's own handle reads the same counters as the one passed in,
1197		// rather than a private set the scraper would never see.
1198		let own = handle.metrics().snapshot();
1199		assert_eq!((own.tx_sends, own.tx_datagrams), (snap.tx_sends, snap.tx_datagrams));
1200	}
1201
1202	/// The backpressure counters are the first thing to look at when throughput
1203	/// sags, so both ends of it have to be recorded: a send that found the pool
1204	/// drained, and a receive that could not be re-armed for want of a buffer.
1205	#[test]
1206	fn metrics_record_pool_backpressure() {
1207		let metrics = Metrics::default();
1208		let config = Config {
1209			metrics: metrics.clone(),
1210			..Default::default()
1211		};
1212		let Some(mut worker) = worker_with(config) else { return };
1213		let handle = worker.handle();
1214		// One buffer each way, so the pools are at their ceiling immediately.
1215		// Oneshot receives claim a whole buffer, which is what lets a held
1216		// packet leave the socket unarmed.
1217		let sock = handle
1218			.udp(
1219				std::net::UdpSocket::bind("127.0.0.1:0").expect("bind"),
1220				udp::Config {
1221					gro: false,
1222					gso: false,
1223					multishot: false,
1224					rx_buffers_max: 1,
1225					rx_buffer_len: 2048,
1226					tx_buffers_max: 1,
1227					tx_buffer_len: 2048,
1228				},
1229			)
1230			.expect("socket");
1231		let to = sock.local_addr().expect("addr");
1232
1233		let Poll::Ready(Ok(tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
1234			panic!("no tx buffer");
1235		};
1236		// The pool is one buffer deep and that one is checked out.
1237		assert!(sock.poll_acquire(&kio::Waiter::noop()).is_pending());
1238		assert!(sock.poll_acquire(&kio::Waiter::noop()).is_pending());
1239		assert_eq!(metrics.snapshot().tx_stalls, 1);
1240		tx.send(udp::Transmit {
1241			to,
1242			len: 1200,
1243			segment: 1200,
1244			ecn: None,
1245		})
1246		.expect("send");
1247
1248		// Hold the received packet: its buffer is the pool, so the re-arm has
1249		// nowhere to receive into.
1250		let deadline = Instant::now() + Duration::from_secs(5);
1251		let mut held = None;
1252		while held.is_none() && Instant::now() < deadline {
1253			let handle = handle.clone();
1254			worker
1255				.block_on(async move {
1256					Deadline::after(&handle, Duration::from_millis(10)).wait().await;
1257				})
1258				.unwrap();
1259			if let Poll::Ready(packet) = sock.poll_recv(&kio::Waiter::noop()) {
1260				held = Some(packet.expect("receive path failed"));
1261			}
1262		}
1263		assert!(held.is_some(), "the loopback never delivered the send");
1264		assert!(
1265			metrics.snapshot().rx_exhausted > 0,
1266			"a re-arm with every buffer held went unreported: {:?}",
1267			metrics.snapshot()
1268		);
1269
1270		// A completed send ends the first stall. Draining the pool again starts
1271		// exactly one new episode, however often its waiter is polled.
1272		let Poll::Ready(Ok(_tx)) = sock.poll_acquire(&kio::Waiter::noop()) else {
1273			panic!("completed tx buffer was not released");
1274		};
1275		assert!(sock.poll_acquire(&kio::Waiter::noop()).is_pending());
1276		assert!(sock.poll_acquire(&kio::Waiter::noop()).is_pending());
1277		assert_eq!(metrics.snapshot().tx_stalls, 2);
1278	}
1279
1280	/// Timer churn is the thing #3122 needs a baseline for, so an arm, a
1281	/// re-arm, and a drop each have to land in a different counter, and the
1282	/// derived heap depth has to come back to zero.
1283	#[test]
1284	fn metrics_count_timer_churn() {
1285		let metrics = Metrics::default();
1286		let config = Config {
1287			metrics: metrics.clone(),
1288			..Default::default()
1289		};
1290		let Some(worker) = worker_with(config) else { return };
1291		let handle = worker.handle();
1292		let mut timer = handle.timer();
1293
1294		timer.set(Some(Instant::now() + Duration::from_secs(60)));
1295		assert_eq!(metrics.snapshot().timers_active(), 1);
1296
1297		// A re-arm is a cancel plus an arm, which is exactly the churn signal.
1298		timer.set(Some(Instant::now() + Duration::from_secs(60)));
1299		let snap = metrics.snapshot();
1300		assert_eq!((snap.timers_armed, snap.timers_cancelled, snap.timers_fired), (2, 1, 0));
1301		assert_eq!(snap.timers_active(), 1);
1302
1303		// An eager poll past the deadline fires rather than cancels.
1304		timer.set(Some(Instant::now() - Duration::from_millis(1)));
1305		assert!(timer.poll(&kio::Waiter::noop()).is_ready());
1306		let snap = metrics.snapshot();
1307		assert_eq!((snap.timers_armed, snap.timers_cancelled, snap.timers_fired), (3, 2, 1));
1308		assert_eq!(snap.timers_active(), 0);
1309
1310		// A dropped armed timer is a cancel, and the heap empties again.
1311		timer.set(Some(Instant::now() + Duration::from_secs(60)));
1312		assert_eq!(metrics.snapshot().timers_active(), 1);
1313		drop(timer);
1314		assert_eq!(metrics.snapshot().timers_active(), 0);
1315	}
1316
1317	#[test]
1318	fn remote_wake_unparks() {
1319		let metrics = Metrics::default();
1320		let config = Config {
1321			metrics: metrics.clone(),
1322			..Default::default()
1323		};
1324		let Some(mut worker) = worker_with(config) else { return };
1325		let flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1326
1327		let thread_flag = flag.clone();
1328		let waker_slot = std::sync::Arc::new(std::sync::Mutex::new(None::<std::task::Waker>));
1329		let thread_slot = waker_slot.clone();
1330		let thread = std::thread::spawn(move || {
1331			// Wait until the worker has parked on the future below.
1332			std::thread::sleep(Duration::from_millis(50));
1333			thread_flag.store(true, Ordering::Release);
1334			if let Some(waker) = thread_slot.lock().unwrap().take() {
1335				waker.wake();
1336			}
1337		});
1338
1339		let start = Instant::now();
1340		worker
1341			.block_on(std::future::poll_fn(move |cx| {
1342				if flag.load(Ordering::Acquire) {
1343					return Poll::Ready(());
1344				}
1345				*waker_slot.lock().unwrap() = Some(cx.waker().clone());
1346				Poll::Pending
1347			}))
1348			.unwrap();
1349		assert!(start.elapsed() >= Duration::from_millis(50));
1350		thread.join().unwrap();
1351		// The futex syscall the wake had to make is the expensive half, and the
1352		// only counter written from off the worker's thread.
1353		assert!(metrics.snapshot().wakes > 0, "the remote wake went unreported");
1354	}
1355}