Skip to main content

subetha_cxc/
reactor.rs

1//! `reactor`: the bridge that makes a SubEtha ring a first-class async
2//! source ACROSS processes, not just across threads.
3//!
4//! Intra-process async is direct: the producer holds the consumer's
5//! `Waker` and fires it on push (see [`crate::waker_ring`]). Across
6//! processes the producer is in another address space and cannot touch
7//! a local `Waker`, so a parked future needs something in THIS process
8//! to notice the cross-process publish and fire its `Waker`. That is
9//! the reactor: one background thread per process that blocks on the
10//! MMF [`CrossProcessWaker`], and when another process publishes, fires
11//! the local `Waker` of the
12//! future parked on the ring. It is the epoll/IOCP reactor pattern with
13//! the readiness source being a shared-memory ring head instead of a
14//! socket.
15//!
16//! # One surface, two locales
17//!
18//! [`ReactiveReceiver::recv`] returns the same future whether the
19//! producer is a thread or a process:
20//!  - [`anon_pair`] builds an intra-process channel; the sender fires
21//!    the receiver's `Waker` directly, no reactor thread.
22//!  - [`receiver_cross`] / [`sender_cross`] build the cross-process
23//!    halves over a shared MMF ring + named waker; a reactor thread in
24//!    the consumer bridges the publish to the local `Waker`.
25//!
26//! Unlike [`crate::async_ring`], which spawns one OS thread per
27//! in-flight future, the reactor uses ONE thread per process regardless
28//! of how many futures park on the ring.
29//!
30//! # `block_on`
31//!
32//! [`block_on`] is a minimal thread-parking driver: it sleeps the
33//! calling thread between polls and is unparked by the future's `Waker`.
34//! Paired with the reactor, a consumer process genuinely sleeps (both
35//! the driver thread and the reactor thread park in the kernel) until
36//! another process publishes - no busy-spin.
37
38use std::future::Future;
39use std::pin::Pin;
40use std::sync::Arc;
41use std::sync::atomic::{AtomicBool, Ordering};
42use std::task::{Context, Poll, Wake, Waker};
43use std::thread::JoinHandle;
44use std::time::Duration;
45
46use parking_lot::Mutex;
47
48use crate::cross_process_waker::CrossProcessWaker;
49use crate::shared_ring::RingError;
50use crate::spsc_ring::{SpscRingCore, SPSC_PAYLOAD_BYTES};
51
52/// Maximum the reactor sleeps per wait before re-checking the ring head,
53/// independent of the cross-process wake. The wake (the common path)
54/// returns far sooner; this tick only matters when a wake is lost to the
55/// `CrossProcessWaker` register/wake visibility race, which the head
56/// re-check at the loop top then heals. Bounded, so a lost wake cannot
57/// hang the consumer; large enough that an idle reactor barely ticks.
58const REACTOR_HEAL_INTERVAL: Duration = Duration::from_millis(50);
59
60/// A `Waker` that unparks a specific thread. The driver behind
61/// [`block_on`].
62struct ThreadWaker {
63    thread: std::thread::Thread,
64}
65
66impl Wake for ThreadWaker {
67    fn wake(self: Arc<Self>) {
68        self.thread.unpark();
69    }
70    fn wake_by_ref(self: &Arc<Self>) {
71        self.thread.unpark();
72    }
73}
74
75/// Drive a future to completion on the current thread, parking the
76/// thread between polls. The future's `Waker` unparks it; a reactor
77/// (or a local sender) fires that `Waker` on readiness.
78pub fn block_on<F: Future>(future: F) -> F::Output {
79    let mut future = Box::pin(future);
80    let waker = Waker::from(Arc::new(ThreadWaker {
81        thread: std::thread::current(),
82    }));
83    let mut cx = Context::from_waker(&waker);
84    loop {
85        match future.as_mut().poll(&mut cx) {
86            Poll::Ready(v) => return v,
87            // A spurious unpark just re-polls (which re-checks the ring
88            // and re-parks), so this is correct without a flag.
89            Poll::Pending => std::thread::park(),
90        }
91    }
92}
93
94/// Where a sender's push delivers its readiness signal.
95enum SenderSignal {
96    /// Intra-process: fire the receiver's `Waker` directly.
97    Local(Arc<Mutex<Option<Waker>>>),
98    /// Cross-process: wake the consumer's reactor through the MMF.
99    Cross(Arc<CrossProcessWaker>),
100}
101
102/// Producer half. `try_send` publishes the payload and signals the
103/// consumer - a direct `Waker` fire intra-process, an MMF wake
104/// cross-process.
105pub struct ReactiveSender {
106    ring: Arc<SpscRingCore>,
107    signal: SenderSignal,
108}
109
110impl ReactiveSender {
111    /// Push a payload and signal the consumer. Returns `Err(Full)` when
112    /// the ring is full (the signal is sent only on a successful push).
113    pub fn try_send(&self, payload: &[u8]) -> Result<(), RingError> {
114        self.ring.try_push(payload)?;
115        match &self.signal {
116            SenderSignal::Local(slot) => {
117                if let Some(w) = slot.lock().take() {
118                    w.wake();
119                }
120            }
121            SenderSignal::Cross(xwaker) => {
122                xwaker.wake_up_to(self.ring.head());
123            }
124        }
125        Ok(())
126    }
127
128    /// The producer's published item count (ring head).
129    pub fn published(&self) -> u64 {
130        self.ring.head()
131    }
132}
133
134/// Consumer half. `recv()` is an `.await`-able future that resolves
135/// when an item arrives, suspending the task until then - off-thread
136/// across threads OR across processes, behind the same call.
137pub struct ReactiveReceiver {
138    ring: Arc<SpscRingCore>,
139    slot: Arc<Mutex<Option<Waker>>>,
140    /// Present only in cross-process mode; owns the reactor thread and
141    /// stops it on drop.
142    _reactor: Option<ReactorHandle>,
143}
144
145impl ReactiveReceiver {
146    /// A future resolving to the next slot's bytes. Owns clones of the
147    /// ring + waker slot, so it is `Send + 'static`.
148    pub fn recv(&self) -> ReactiveRecv {
149        ReactiveRecv {
150            ring: Arc::clone(&self.ring),
151            slot: Arc::clone(&self.slot),
152        }
153    }
154
155    /// Non-blocking pop, for draining without awaiting.
156    pub fn try_recv(&self, out: &mut [u8]) -> Result<usize, RingError> {
157        self.ring.try_pop(out)
158    }
159}
160
161/// Future returned by [`ReactiveReceiver::recv`].
162pub struct ReactiveRecv {
163    ring: Arc<SpscRingCore>,
164    slot: Arc<Mutex<Option<Waker>>>,
165}
166
167impl Future for ReactiveRecv {
168    type Output = [u8; SPSC_PAYLOAD_BYTES];
169
170    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
171        let mut out = [0u8; SPSC_PAYLOAD_BYTES];
172        if self.ring.try_pop(&mut out).is_ok() {
173            return Poll::Ready(out);
174        }
175        // Register, then re-check: an item that landed between the first
176        // pop and this registration is caught here, not lost.
177        *self.slot.lock() = Some(cx.waker().clone());
178        if self.ring.try_pop(&mut out).is_ok() {
179            return Poll::Ready(out);
180        }
181        Poll::Pending
182    }
183}
184
185/// Owns the reactor thread and stops it when the receiver drops.
186struct ReactorHandle {
187    shutdown: Arc<AtomicBool>,
188    xwaker: Arc<CrossProcessWaker>,
189    join: Option<JoinHandle<()>>,
190}
191
192impl Drop for ReactorHandle {
193    fn drop(&mut self) {
194        self.shutdown.store(true, Ordering::Release);
195        // Unblock the reactor's wait() so it sees the shutdown flag.
196        self.xwaker.wake_all();
197        if let Some(j) = self.join.take() {
198            j.join().ok();
199        }
200    }
201}
202
203/// Intra-process reactive channel: the sender fires the receiver's
204/// `Waker` directly on push. No reactor thread. `capacity` must be a
205/// power of two.
206pub fn anon_pair(
207    capacity: usize,
208) -> Result<(ReactiveSender, ReactiveReceiver), RingError> {
209    let ring = Arc::new(SpscRingCore::create_anon(capacity)?);
210    let slot = Arc::new(Mutex::new(None));
211    Ok((
212        ReactiveSender {
213            ring: Arc::clone(&ring),
214            signal: SenderSignal::Local(Arc::clone(&slot)),
215        },
216        ReactiveReceiver { ring, slot, _reactor: None },
217    ))
218}
219
220/// Cross-process producer half over a shared MMF ring + named waker.
221/// The two processes share the same ring file and the same waker file.
222pub fn sender_cross(
223    ring: Arc<SpscRingCore>,
224    xwaker: Arc<CrossProcessWaker>,
225) -> ReactiveSender {
226    ReactiveSender { ring, signal: SenderSignal::Cross(xwaker) }
227}
228
229/// Cross-process consumer half. Spawns a reactor thread that blocks on
230/// the shared waker and fires the local `Waker` of the future parked on
231/// the ring whenever the producer process publishes.
232pub fn receiver_cross(
233    ring: Arc<SpscRingCore>,
234    xwaker: Arc<CrossProcessWaker>,
235) -> ReactiveReceiver {
236    let slot: Arc<Mutex<Option<Waker>>> = Arc::new(Mutex::new(None));
237    let shutdown = Arc::new(AtomicBool::new(false));
238
239    let join = {
240        let ring = Arc::clone(&ring);
241        let xwaker = Arc::clone(&xwaker);
242        let slot = Arc::clone(&slot);
243        let shutdown = Arc::clone(&shutdown);
244        std::thread::spawn(move || reactor_loop(ring, xwaker, slot, shutdown))
245    };
246
247    ReactiveReceiver {
248        ring,
249        slot,
250        _reactor: Some(ReactorHandle {
251            shutdown,
252            xwaker,
253            join: Some(join),
254        }),
255    }
256}
257
258/// The reactor: bridge cross-process publishes to the local `Waker`.
259/// Blocks on the MMF waker; on every observed head advance fires the
260/// parked future's `Waker` so the driver re-polls and pops.
261fn reactor_loop(
262    ring: Arc<SpscRingCore>,
263    xwaker: Arc<CrossProcessWaker>,
264    slot: Arc<Mutex<Option<Waker>>>,
265    shutdown: Arc<AtomicBool>,
266) {
267    let mut last = ring.head();
268    loop {
269        if shutdown.load(Ordering::Acquire) {
270            break;
271        }
272        let head = ring.head();
273        if head != last {
274            last = head;
275            if let Some(w) = slot.lock().take() {
276                w.wake();
277            }
278            continue;
279        }
280        // Park until the producer publishes past `head`.
281        match xwaker.try_park(head + 1) {
282            Ok(token) => {
283                // Lost-wake guard: an item that landed (or a shutdown
284                // that fired) between the head read and the park is
285                // caught here.
286                if shutdown.load(Ordering::Acquire) || ring.head() != head {
287                    xwaker.release(token);
288                    continue;
289                }
290                // Heal-bounded: a real cross-process wake (producer
291                // publish) or the shutdown `wake_all` ends the wait fast;
292                // the bounded tick is the backstop so a wake lost to the
293                // register/visibility race self-heals at the loop top
294                // (head re-check) instead of hanging the consumer.
295                xwaker.wait(token, Some(REACTOR_HEAL_INTERVAL)).ok();
296            }
297            Err(crate::cross_process_waker::WakerError::Full) => {
298                // No free waker slot; re-check shortly.
299                std::hint::spin_loop();
300            }
301            Err(_) => break,
302        }
303    }
304}
305
306/// A bridge from an arbitrary monotonic published-seq source to a local
307/// `Waker` slot, for channels whose backing is not an `SpscRingCore`
308/// (e.g. a [`SharedRing`](crate::SharedRing)'s `producer_seq` /
309/// `consumer_seq`). Same heal-bounded loop as [`reactor_loop`]; the
310/// closure supplies the count. Stops its thread on drop.
311pub(crate) struct SeqReactor {
312    shutdown: Arc<AtomicBool>,
313    xwaker: Arc<CrossProcessWaker>,
314    join: Option<JoinHandle<()>>,
315}
316
317impl Drop for SeqReactor {
318    fn drop(&mut self) {
319        self.shutdown.store(true, Ordering::Release);
320        self.xwaker.wake_all();
321        if let Some(j) = self.join.take() {
322            j.join().ok();
323        }
324    }
325}
326
327/// Spawn a reactor firing `slot` whenever `published()` advances,
328/// parking on `xwaker` between observations.
329pub(crate) fn spawn_seq_reactor(
330    published: Arc<dyn Fn() -> u64 + Send + Sync>,
331    xwaker: Arc<CrossProcessWaker>,
332    slot: Arc<Mutex<Option<Waker>>>,
333) -> SeqReactor {
334    let shutdown = Arc::new(AtomicBool::new(false));
335    let join = {
336        let xwaker = Arc::clone(&xwaker);
337        let shutdown = Arc::clone(&shutdown);
338        std::thread::spawn(move || seq_reactor_loop(published, &xwaker, &slot, &shutdown))
339    };
340    SeqReactor { shutdown, xwaker, join: Some(join) }
341}
342
343fn seq_reactor_loop(
344    published: Arc<dyn Fn() -> u64 + Send + Sync>,
345    xwaker: &CrossProcessWaker,
346    slot: &Mutex<Option<Waker>>,
347    shutdown: &AtomicBool,
348) {
349    let mut last = published();
350    loop {
351        if shutdown.load(Ordering::Acquire) {
352            break;
353        }
354        let cur = published();
355        if cur != last {
356            last = cur;
357            if let Some(w) = slot.lock().take() {
358                w.wake();
359            }
360            continue;
361        }
362        match xwaker.try_park(cur + 1) {
363            Ok(token) => {
364                if shutdown.load(Ordering::Acquire) || published() != cur {
365                    xwaker.release(token);
366                    continue;
367                }
368                xwaker.wait(token, Some(REACTOR_HEAL_INTERVAL)).ok();
369            }
370            Err(crate::cross_process_waker::WakerError::Full) => {
371                std::hint::spin_loop();
372            }
373            Err(_) => break,
374        }
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use std::sync::atomic::{AtomicU64, Ordering};
382    use std::thread;
383
384    #[test]
385    fn intra_process_block_on_parks_and_wakes_on_send() {
386        // The driver thread parks between items; the sender's direct
387        // Waker fire unparks it. Proves the recv future suspends rather
388        // than spins, with no reactor thread in this locale.
389        let (tx, rx) = anon_pair(8).unwrap();
390        const N: u64 = 1000;
391
392        let producer = thread::spawn(move || {
393            for i in 0..N {
394                let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
395                buf[..8].copy_from_slice(&i.to_le_bytes());
396                while tx.try_send(&buf).is_err() {
397                    std::hint::spin_loop();
398                }
399            }
400        });
401
402        let sum = block_on(async move {
403            let mut s = 0u64;
404            for _ in 0..N {
405                let item = rx.recv().await;
406                s += u64::from_le_bytes(item[..8].try_into().unwrap());
407            }
408            s
409        });
410
411        producer.join().unwrap();
412        assert_eq!(sum, (0..N).sum());
413    }
414
415    #[test]
416    fn many_futures_one_local_signal() {
417        // A second consumer task (driven on a worker) also wakes on the
418        // same channel's sender fire; verifies the slot-register /
419        // re-check path under interleaving.
420        let (tx, rx) = anon_pair(4).unwrap();
421        let got = Arc::new(AtomicU64::new(0));
422        let got2 = Arc::clone(&got);
423
424        let consumer = thread::spawn(move || {
425            block_on(async move {
426                for _ in 0..500u64 {
427                    let item = rx.recv().await;
428                    got2.fetch_add(
429                        u64::from_le_bytes(item[..8].try_into().unwrap()),
430                        Ordering::AcqRel,
431                    );
432                }
433            });
434        });
435
436        for i in 0..500u64 {
437            let mut buf = [0u8; SPSC_PAYLOAD_BYTES];
438            buf[..8].copy_from_slice(&i.to_le_bytes());
439            while tx.try_send(&buf).is_err() {
440                std::hint::spin_loop();
441            }
442        }
443        consumer.join().unwrap();
444        assert_eq!(got.load(Ordering::Acquire), (0..500u64).sum());
445    }
446}