Skip to main content

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}