Skip to main content

proxima_core/
signal.rs

1//! Sticky one-shot level signal — the cancellation / shutdown scope
2//! primitive.
3//!
4//! A [`Signal`] is a *level*, not an event: it transitions once
5//! (unfired → fired) and stays fired, so a late observer polls an
6//! already-fired signal and resolves immediately — the
7//! notify-before-wait lost-wakeup class cannot happen by construction.
8//!
9//! Scopes nest by merge, not by tree: [`Signal::child`] observes every
10//! ancestor level plus its own, so firing a parent cancels the whole
11//! subtree while firing a child leaves the parent untouched. A handle
12//! is a flat `Vec<Arc<Level>>` (depth = real scope nesting: process →
13//! listener → connection → request), `is_fired` is one relaxed pass of
14//! atomic loads, and there is no node graph to refcount.
15
16// Only the racy primitives (the fired flag + the waiters mutex) swap to
17// loom's instrumented equivalents under `--features loom`, matching
18// `ring/mpsc.rs`'s own convention (see that module's doc comment): loom
19// explores interleavings of the types it instruments, so `Arc` — plain
20// refcounting, not part of the fire/register/drop race — stays `std::sync::Arc`
21// unconditionally. Rewiring it to `loom::sync::Arc` would also break the
22// `Arc<[Arc<Level>]>` unsized-slice construction (`Arc::from([..])`), which
23// loom's mock `Arc` does not support (no `CoerceUnsized`; see
24// `loom::sync::Arc::from_std`'s doc comment) — a real API gap, not a choice.
25#[cfg(feature = "loom")]
26use loom::sync::Mutex;
27#[cfg(feature = "loom")]
28use loom::sync::atomic::{AtomicBool, Ordering};
29use std::sync::{Arc, OnceLock};
30#[cfg(not(feature = "loom"))]
31use std::sync::Mutex;
32#[cfg(not(feature = "loom"))]
33use std::sync::atomic::{AtomicBool, Ordering};
34use std::task::{Context, Poll, Waker};
35
36use core::future::Future;
37use core::pin::Pin;
38
39use futures::task::AtomicWaker;
40use smallvec::SmallVec;
41
42/// One awaiting observer's waker cell. Each [`Fired`] future owns one slot
43/// and registers it with every unfired level; re-polls update the slot in
44/// place, so a select loop re-polling never grows the registries.
45///
46/// Backed by [`AtomicWaker`] (from `futures`, RISC reuse per principle 1)
47/// rather than a hand-rolled `Mutex<Option<Waker>>`: it is lock-free (no
48/// blocking mutex on the register/wake path, per principle 21) and its
49/// `register`-then-caller-checks-again contract is the exact same
50/// lost-wakeup-avoidance shape `Fired::poll` already implements.
51struct WakerSlot(AtomicWaker);
52
53impl WakerSlot {
54    fn new() -> Self {
55        Self(AtomicWaker::new())
56    }
57
58    fn register(&self, waker: &Waker) {
59        self.0.register(waker);
60    }
61
62    fn wake(&self) {
63        self.0.wake();
64    }
65}
66
67/// A level's set of pending waiters. Inline-capacity `SmallVec` (cap sized by
68/// `sized::SIGNAL_WAITERS_INLINE_CAP`, principle 12): the common case — a
69/// handful of concurrent `Fired` futures awaiting one scope level — never
70/// touches the allocator, in place of the previous unconditionally-heap Vec.
71/// A burst past the inline cap spills to a heap-backed tail, same as Vec
72/// always did — no waiter is ever refused registration.
73type WaiterRegistry = SmallVec<[Arc<WakerSlot>; crate::sized::SIGNAL_WAITERS_INLINE_CAP]>;
74
75struct Level {
76    fired: AtomicBool,
77    waiters: Mutex<WaiterRegistry>,
78}
79
80impl Level {
81    fn new() -> Arc<Self> {
82        Arc::new(Self {
83            fired: AtomicBool::new(false),
84            waiters: Mutex::new(SmallVec::new()),
85        })
86    }
87
88    // a panic while some other thread held the registry poisons it, but the
89    // registry is just a list of waker slots — structurally intact either way.
90    // Recovering through into_inner is what keeps a poisoned lock from turning
91    // a shutdown signal into a silent hang. Same handling as time::drivers::mock.
92    fn waiters(&self) -> impl core::ops::DerefMut<Target = WaiterRegistry> + '_ {
93        self.waiters.lock().unwrap_or_else(|err| err.into_inner())
94    }
95
96    fn fire(&self) {
97        if self.fired.swap(true, Ordering::AcqRel) {
98            return;
99        }
100        let drained = core::mem::take(&mut *self.waiters());
101        for slot in drained {
102            slot.wake();
103        }
104    }
105
106    fn is_fired(&self) -> bool {
107        self.fired.load(Ordering::Acquire)
108    }
109}
110
111/// Cloneable handle onto a scope's cancellation level. See the module
112/// docs for the level/merge model.
113///
114/// The level list is a shared slice, not an owned `Vec`: clone (the
115/// hot per-request/per-record operation) is one atomic increment and
116/// zero allocation; only `new`/`child` (scope creation) allocate.
117#[derive(Clone)]
118pub struct Signal {
119    levels: Arc<[Arc<Level>]>,
120}
121
122impl Signal {
123    #[must_use]
124    pub fn new() -> Self {
125        Self {
126            levels: Arc::from([Level::new()]),
127        }
128    }
129
130    /// A nested scope: observes every ancestor level plus a fresh one
131    /// of its own. Firing the child never touches the ancestors.
132    #[must_use]
133    pub fn child(&self) -> Self {
134        let mut levels = Vec::with_capacity(self.levels.len() + 1);
135        levels.extend(self.levels.iter().cloned());
136        levels.push(Level::new());
137        Self {
138            levels: Arc::from(levels),
139        }
140    }
141
142    /// Fire this handle's own scope level (idempotent). Ancestor
143    /// levels are never fired through a child handle.
144    pub fn fire(&self) {
145        if let Some(own) = self.levels.last() {
146            own.fire();
147        }
148    }
149
150    /// True once this scope or any ancestor scope has fired.
151    #[must_use]
152    pub fn is_fired(&self) -> bool {
153        self.levels.iter().any(|level| level.is_fired())
154    }
155
156    /// Resolves when this scope or any ancestor fires; resolves
157    /// immediately if that already happened (sticky level).
158    #[must_use]
159    pub fn fired(&self) -> Fired {
160        Fired {
161            levels: self.levels.clone(),
162            slot: OnceLock::new(),
163        }
164    }
165
166    /// Fire-on-drop guard for tying a scope's lifetime to a value
167    /// (e.g. cancel a request's subtree when its driver unwinds).
168    #[must_use]
169    pub fn guard(self) -> SignalGuard {
170        SignalGuard {
171            signal: self,
172            armed: true,
173        }
174    }
175}
176
177impl Default for Signal {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183impl core::fmt::Debug for Signal {
184    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
185        formatter
186            .debug_struct("Signal")
187            .field("depth", &self.levels.len())
188            .field("fired", &self.is_fired())
189            .finish()
190    }
191}
192
193/// Future returned by [`Signal::fired`]. Owns its level handles, so it
194/// is `'static` and can outlive the handle it came from.
195pub struct Fired {
196    levels: Arc<[Arc<Level>]>,
197    slot: OnceLock<Arc<WakerSlot>>,
198}
199
200impl Fired {
201    /// Poll through a shared reference, for an owner that must keep this
202    /// registration alive across abandoned call futures (`FanIn` drops a
203    /// `Pending` call every scan). The waker slot lives in `self`, so it is
204    /// unregistered only when the `Fired` itself drops; a re-poll refreshes
205    /// the slot's waker in place. Prefer `.await` / `Future::poll` when the
206    /// future is polled to completion by one owner.
207    pub fn poll_shared(&self, cx: &mut Context<'_>) -> Poll<()> {
208        // register-then-check closes the lost-wakeup race: fire() sets
209        // the flag before draining waiters, so a fire that slipped past
210        // our registration is visible in the check below.
211        let slot = self.slot.get_or_init(|| {
212            let slot = Arc::new(WakerSlot::new());
213            for level in self.levels.iter() {
214                level.waiters().push(slot.clone());
215            }
216            slot
217        });
218        slot.register(cx.waker());
219        if self.levels.iter().any(|level| level.is_fired()) {
220            Poll::Ready(())
221        } else {
222            Poll::Pending
223        }
224    }
225}
226
227impl Future for Fired {
228    type Output = ();
229
230    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
231        self.poll_shared(cx)
232    }
233}
234
235impl Drop for Fired {
236    fn drop(&mut self) {
237        let Some(slot) = self.slot.take() else {
238            return;
239        };
240        for level in self.levels.iter() {
241            level
242                .waiters()
243                .retain(|candidate| !Arc::ptr_eq(candidate, &slot));
244        }
245    }
246}
247
248/// Fires the wrapped signal's own level on drop, unless disarmed.
249pub struct SignalGuard {
250    signal: Signal,
251    armed: bool,
252}
253
254impl SignalGuard {
255    #[must_use]
256    pub fn signal(&self) -> &Signal {
257        &self.signal
258    }
259
260    /// Consume the guard without firing — the success path, where the
261    /// scope outlives the guard. Returns the signal untouched.
262    pub fn disarm(mut self) -> Signal {
263        self.armed = false;
264        self.signal.clone()
265    }
266}
267
268impl Drop for SignalGuard {
269    fn drop(&mut self) {
270        if self.armed {
271            self.signal.fire();
272        }
273    }
274}
275
276// the fired flag + waiters mutex are cfg-swapped to loom under `--features
277// loom` (see the note at the top of this module) — those only work inside an
278// actual loom::model(...) closure, which these plain #[test] functions don't
279// provide. Same gate ring/mpsc.rs and ring/bounded.rs already carry; the loom
280// coverage for this module is tests/loom_signal.rs.
281#[cfg(all(test, not(feature = "loom")))]
282#[allow(clippy::unwrap_used, clippy::expect_used)]
283mod tests {
284    use std::panic::{AssertUnwindSafe, catch_unwind};
285    use std::sync::atomic::AtomicUsize;
286    use std::task::Wake;
287
288    use super::*;
289
290    struct CountingWaker(AtomicUsize);
291
292    impl Wake for CountingWaker {
293        fn wake(self: Arc<Self>) {
294            self.0.fetch_add(1, Ordering::Relaxed);
295        }
296    }
297
298    fn counting_waker() -> (Arc<CountingWaker>, Waker) {
299        let counter = Arc::new(CountingWaker(AtomicUsize::new(0)));
300        let waker = Waker::from(counter.clone());
301        (counter, waker)
302    }
303
304    #[test]
305    fn fire_is_sticky_and_idempotent() {
306        let signal = Signal::new();
307        assert!(!signal.is_fired());
308        signal.fire();
309        signal.fire();
310        assert!(signal.is_fired());
311    }
312
313    #[test]
314    fn late_observer_resolves_on_first_poll() {
315        let signal = Signal::new();
316        signal.fire();
317        let mut fired = signal.fired();
318        let (_, waker) = counting_waker();
319        let mut cx = Context::from_waker(&waker);
320        assert_eq!(Pin::new(&mut fired).poll(&mut cx), Poll::Ready(()));
321    }
322
323    #[test]
324    fn registered_waiter_is_woken_by_fire() {
325        let signal = Signal::new();
326        let mut fired = signal.fired();
327        let (counter, waker) = counting_waker();
328        let mut cx = Context::from_waker(&waker);
329        assert_eq!(Pin::new(&mut fired).poll(&mut cx), Poll::Pending);
330        signal.fire();
331        assert_eq!(counter.0.load(Ordering::Relaxed), 1);
332        assert_eq!(Pin::new(&mut fired).poll(&mut cx), Poll::Ready(()));
333    }
334
335    #[test]
336    fn child_observes_parent_fire() {
337        let parent = Signal::new();
338        let child = parent.child();
339        parent.fire();
340        assert!(child.is_fired());
341    }
342
343    #[test]
344    fn parent_is_untouched_by_child_fire() {
345        let parent = Signal::new();
346        let child = parent.child();
347        child.fire();
348        assert!(child.is_fired());
349        assert!(!parent.is_fired());
350    }
351
352    #[test]
353    fn grandchild_observes_root_through_the_merge() {
354        let root = Signal::new();
355        let grandchild = root.child().child();
356        root.fire();
357        assert!(grandchild.is_fired());
358    }
359
360    #[test]
361    fn guard_fires_own_level_on_drop() {
362        let signal = Signal::new();
363        let observer = signal.clone();
364        {
365            let _guard = signal.guard();
366            assert!(!observer.is_fired());
367        }
368        assert!(observer.is_fired());
369    }
370
371    #[test]
372    fn disarmed_guard_does_not_fire_on_drop() {
373        let signal = Signal::new();
374        let observer = signal.clone();
375        let guard = signal.guard();
376        let returned = guard.disarm();
377        assert!(!observer.is_fired());
378        assert!(!returned.is_fired());
379    }
380
381    #[test]
382    fn dropping_a_pending_waiter_unregisters_its_slot() {
383        let signal = Signal::new();
384        let (_, waker) = counting_waker();
385        let mut cx = Context::from_waker(&waker);
386        {
387            let mut fired = signal.fired();
388            assert_eq!(Pin::new(&mut fired).poll(&mut cx), Poll::Pending);
389            assert_eq!(signal.levels[0].waiters.lock().unwrap().len(), 1);
390        }
391        assert_eq!(signal.levels[0].waiters.lock().unwrap().len(), 0);
392    }
393
394    // a shutdown signal that silently declines to fire is a hang, so a panic
395    // elsewhere must not be able to disarm it. The registry is only a list of
396    // waker slots, so recovering the poisoned guard is sound.
397    #[test]
398    fn a_poisoned_waiter_registry_still_wakes_its_waiters() {
399        let signal = Signal::new();
400        let mut fired = signal.fired();
401        let (counter, waker) = counting_waker();
402        let mut cx = Context::from_waker(&waker);
403        assert_eq!(Pin::new(&mut fired).poll(&mut cx), Poll::Pending);
404
405        let poisoner = signal.clone();
406        let outcome = catch_unwind(AssertUnwindSafe(|| {
407            let _held = poisoner.levels[0].waiters.lock().expect("uncontended lock");
408            panic!("poison the waiter registry");
409        }));
410        assert!(outcome.is_err(), "the poisoning closure must have unwound");
411        assert!(signal.levels[0].waiters.is_poisoned());
412
413        signal.fire();
414
415        assert_eq!(
416            counter.0.load(Ordering::Relaxed),
417            1,
418            "a poisoned registry must still deliver the wakeup"
419        );
420        assert_eq!(Pin::new(&mut fired).poll(&mut cx), Poll::Ready(()));
421    }
422
423    #[test]
424    fn repolling_does_not_grow_the_waiter_list() {
425        let signal = Signal::new();
426        let mut fired = signal.fired();
427        let (_, waker) = counting_waker();
428        let mut cx = Context::from_waker(&waker);
429        for _ in 0..64 {
430            assert_eq!(Pin::new(&mut fired).poll(&mut cx), Poll::Pending);
431        }
432        assert_eq!(signal.levels[0].waiters.lock().unwrap().len(), 1);
433    }
434}