subetha_cxc/waker_ring.rs
1//! `WakerRing`: a thread-free async ring. The producer fires the
2//! consumer task's `Waker` directly on push - no worker thread per
3//! awaiting task, no reactor, no syscall on the wake path.
4//!
5//! This is the piece that lets a bounded pool serve an unbounded set of
6//! awaiting subscribers. Contrast [`crate::async_ring::AsyncSpscRing`],
7//! which spawns one OS thread per in-flight future to do the blocking
8//! recv: fine for a handful of long-running tasks, wrong for ten
9//! thousand. Here a consumer that finds the ring empty parks its
10//! `Waker` in a process-local cell beside the ring; the producer's push
11//! takes that `Waker` and wakes it, which re-enqueues the task on
12//! whatever executor is driving it (including [`crate::task_pool`]).
13//!
14//! Why no OS primitive is involved: the producer and consumer share an
15//! address space, so the producer can call the consumer's `Waker`
16//! directly. There is no kernel in the loop and nothing platform-
17//! specific - it runs the same on Windows and Linux. (The cross-process
18//! case is the one that needs a per-process reactor bridging the MMF
19//! `CrossProcessWaker` to local `Waker`s; this module is the intra-
20//! process foundation that reactor reuses.)
21//!
22//! The lost-wake race is closed by the standard register-then-recheck:
23//! a poll that finds the ring empty registers its `Waker`, then checks
24//! the ring ONCE more before returning `Pending`, so an item that
25//! landed between the first check and the registration is never missed.
26
27use std::future::Future;
28use std::pin::Pin;
29use std::sync::Arc;
30use std::task::{Context, Poll, Waker};
31
32use parking_lot::Mutex;
33
34use crate::shared_ring::RingError;
35use crate::spsc_ring::{SpscRingCore, SPSC_PAYLOAD_BYTES};
36
37/// A single-slot home for the consumer task's `Waker`. The producer
38/// `wake()`s it; the consumer `register()`s on each empty poll.
39struct WakerCell {
40 waker: Mutex<Option<Waker>>,
41}
42
43impl WakerCell {
44 fn new() -> Self {
45 Self { waker: Mutex::new(None) }
46 }
47
48 fn register(&self, w: &Waker) {
49 let mut g = self.waker.lock();
50 match g.as_ref() {
51 // Same task re-polling: skip the clone.
52 Some(existing) if existing.will_wake(w) => {}
53 _ => *g = Some(w.clone()),
54 }
55 }
56
57 fn wake(&self) {
58 if let Some(w) = self.waker.lock().take() {
59 w.wake();
60 }
61 }
62}
63
64/// Factory for a thread-free async SPSC pair.
65pub struct WakerRing;
66
67impl WakerRing {
68 /// Anonymous in-process pair: a producer and a consumer sharing one
69 /// SPSC core and one waker cell. `capacity` must be a power of two.
70 pub fn create_anon_pair(
71 capacity: usize,
72 ) -> Result<(WakerProducer, WakerConsumer), RingError> {
73 let ring = Arc::new(SpscRingCore::create_anon(capacity)?);
74 let cell = Arc::new(WakerCell::new());
75 Ok((
76 WakerProducer { ring: Arc::clone(&ring), cell: Arc::clone(&cell) },
77 WakerConsumer { ring, cell },
78 ))
79 }
80}
81
82/// Producer half. `try_push` publishes the item and fires the awaiting
83/// consumer's `Waker` in the same call - no thread, no syscall.
84pub struct WakerProducer {
85 ring: Arc<SpscRingCore>,
86 cell: Arc<WakerCell>,
87}
88
89impl WakerProducer {
90 /// Push a payload; on success, wake the consumer task (if any is
91 /// parked). Returns `Err(Full)` when the ring is full.
92 pub fn try_push(&self, payload: &[u8]) -> Result<(), RingError> {
93 let r = self.ring.try_push(payload);
94 if r.is_ok() {
95 self.cell.wake();
96 }
97 r
98 }
99}
100
101/// Consumer half. `recv()` is an `.await`-able future that resolves
102/// when an item arrives, suspending the task (off-thread) until then.
103pub struct WakerConsumer {
104 ring: Arc<SpscRingCore>,
105 cell: Arc<WakerCell>,
106}
107
108impl WakerConsumer {
109 /// A future that resolves to the next slot's bytes. Spawnable: it
110 /// owns clones of the ring + waker cell, so it is `Send + 'static`.
111 pub fn recv(&self) -> WakerRecv {
112 WakerRecv {
113 ring: Arc::clone(&self.ring),
114 cell: Arc::clone(&self.cell),
115 }
116 }
117
118 /// Non-blocking pop, for draining without awaiting.
119 pub fn try_recv(&self, out: &mut [u8]) -> Result<usize, RingError> {
120 self.ring.try_pop(out)
121 }
122}
123
124/// Future returned by [`WakerConsumer::recv`].
125pub struct WakerRecv {
126 ring: Arc<SpscRingCore>,
127 cell: Arc<WakerCell>,
128}
129
130impl Future for WakerRecv {
131 type Output = [u8; SPSC_PAYLOAD_BYTES];
132
133 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
134 let mut out = [0u8; SPSC_PAYLOAD_BYTES];
135 if self.ring.try_pop(&mut out).is_ok() {
136 return Poll::Ready(out);
137 }
138 // Register, then re-check: an item that landed between the
139 // first pop and this registration is caught here, not lost.
140 self.cell.register(cx.waker());
141 if self.ring.try_pop(&mut out).is_ok() {
142 return Poll::Ready(out);
143 }
144 Poll::Pending
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::task_pool::TaskPool;
152 use std::sync::atomic::{AtomicU64, Ordering};
153
154 #[test]
155 fn many_subscribers_few_threads_no_thread_per_sub() {
156 // N awaiting tasks, a fixed pool. Each task awaits its own ring;
157 // the producer side fires the wakers. If this needed a thread
158 // per subscriber it could not run N >> pool size.
159 let pool = TaskPool::new(2);
160 let n = 4_000u64;
161 let sum = Arc::new(AtomicU64::new(0));
162 let mut producers = Vec::with_capacity(n as usize);
163 for i in 0..n {
164 let (p, c) = WakerRing::create_anon_pair(4).expect("pair");
165 producers.push((i, p));
166 let sum = Arc::clone(&sum);
167 pool.spawn(async move {
168 let got = c.recv().await;
169 let v = u64::from_le_bytes(got[..8].try_into().unwrap());
170 sum.fetch_add(v, Ordering::AcqRel);
171 });
172 }
173 assert_eq!(pool.worker_count(), 2, "N tasks on a 2-thread pool");
174 // Now feed every subscriber exactly once. Each push wakes one
175 // suspended task.
176 let mut payload = [0u8; SPSC_PAYLOAD_BYTES];
177 for (i, p) in &producers {
178 payload[..8].copy_from_slice(&i.to_le_bytes());
179 while p.try_push(&payload).is_err() {
180 std::hint::spin_loop();
181 }
182 }
183 let expected: u64 = (0..n).sum();
184 let start = std::time::Instant::now();
185 while sum.load(Ordering::Acquire) < expected {
186 if start.elapsed() > std::time::Duration::from_secs(10) {
187 panic!("sum {} != expected {expected}", sum.load(Ordering::Acquire));
188 }
189 std::hint::spin_loop();
190 }
191 assert_eq!(sum.load(Ordering::Acquire), expected);
192 pool.shutdown();
193 }
194}