Skip to main content

proxima_primitives/pipe/
signal_source.rs

1//! [`SignalSource`] — an [`UnpinPipe`] face over [`proxima_core::signal::Signal`]
2//! — a cancellation scope as a one-shot source.
3//!
4//! The signal is a sticky level; its source face resolves `Ok(())` exactly
5//! once when the level fires (immediately for a late subscriber) and then
6//! resolves [`Exhausted`] forever after. That makes scope-merge plain source
7//! algebra: `FanIn` over signal sources races cancellation against any other
8//! source without bespoke select plumbing.
9
10use core::future::Future;
11use core::pin::Pin;
12use core::sync::atomic::{AtomicBool, Ordering};
13use core::task::{Context, Poll};
14
15use proxima_core::markers::DropSafe;
16use proxima_core::signal::{Fired, Signal};
17
18use crate::pipe::fan_in::Exhausted;
19use crate::pipe::primitives::UnpinPipe;
20
21/// One observer's source face over a [`Signal`]. Construct per
22/// observer (it tracks whether this observer already yielded).
23pub struct SignalSource {
24    fired: Fired,
25    yielded: AtomicBool,
26}
27
28impl SignalSource {
29    #[must_use]
30    pub fn new(signal: &Signal) -> Self {
31        Self {
32            fired: signal.fired(),
33            yielded: AtomicBool::new(false),
34        }
35    }
36}
37
38impl DropSafe for SignalSource {}
39
40/// The future behind [`SignalSource::call`]. Once already-yielded, resolves
41/// [`Exhausted`] with no further work; otherwise it polls the source's own
42/// [`Fired`], so the waker registration lives in the source and abandoning
43/// this future (as [`crate::pipe::fan_in::FanIn`] does each scan) loses nothing.
44enum SignalCall<'source> {
45    Yielded,
46    Pending { source: &'source SignalSource },
47}
48
49impl Future for SignalCall<'_> {
50    type Output = Result<(), Exhausted>;
51
52    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
53        match self.get_mut() {
54            SignalCall::Yielded => Poll::Ready(Err(Exhausted)),
55            SignalCall::Pending { source } => match source.fired.poll_shared(cx) {
56                Poll::Ready(()) => {
57                    source.yielded.store(true, Ordering::Relaxed);
58                    Poll::Ready(Ok(()))
59                }
60                Poll::Pending => Poll::Pending,
61            },
62        }
63    }
64}
65
66impl SignalSource {
67    fn call_future(&self) -> SignalCall<'_> {
68        if self.yielded.load(Ordering::Relaxed) {
69            SignalCall::Yielded
70        } else {
71            SignalCall::Pending { source: self }
72        }
73    }
74}
75
76impl UnpinPipe for SignalSource {
77    type In = ();
78    type Out = ();
79    type Err = Exhausted;
80
81    fn call(&self, (): ()) -> impl Future<Output = Result<(), Exhausted>> + Unpin {
82        self.call_future()
83    }
84}
85
86#[cfg(test)]
87#[allow(clippy::unwrap_used, clippy::expect_used)]
88mod tests {
89    use std::sync::Arc;
90    use std::sync::atomic::AtomicUsize;
91    use std::task::{Wake, Waker};
92
93    use super::*;
94    use crate::pipe::fan_in::{FanIn, Select};
95    use crate::pipe::primitives::Pipe;
96
97    fn noop_context() -> Context<'static> {
98        Context::from_waker(std::task::Waker::noop())
99    }
100
101    fn call_once(source: &SignalSource, cx: &mut Context<'_>) -> Poll<Result<(), Exhausted>> {
102        let mut call = source.call(());
103        Pin::new(&mut call).poll(cx)
104    }
105
106
107    struct CountingWaker(AtomicUsize);
108
109    impl Wake for CountingWaker {
110        fn wake(self: Arc<Self>) {
111            self.0.fetch_add(1, Ordering::Relaxed);
112        }
113    }
114
115    fn counting_waker() -> (Arc<CountingWaker>, Waker) {
116        let counter = Arc::new(CountingWaker(AtomicUsize::new(0)));
117        let waker = Waker::from(counter.clone());
118        (counter, waker)
119    }
120
121    enum Arm {
122        Cancel(SignalSource),
123        Idle,
124    }
125
126    impl DropSafe for Arm {}
127
128    enum ArmCall<'arm> {
129        Cancel(SignalCall<'arm>),
130        Idle,
131    }
132
133    impl Future for ArmCall<'_> {
134        type Output = Result<(), Exhausted>;
135
136        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
137            match self.get_mut() {
138                ArmCall::Cancel(call) => Pin::new(call).poll(cx),
139                ArmCall::Idle => Poll::Pending,
140            }
141        }
142    }
143
144    impl UnpinPipe for Arm {
145        type In = ();
146        type Out = ();
147        type Err = Exhausted;
148
149        fn call(&self, (): ()) -> impl Future<Output = Result<(), Exhausted>> + Unpin {
150            match self {
151                Arm::Cancel(source) => ArmCall::Cancel(source.call_future()),
152                Arm::Idle => ArmCall::Idle,
153            }
154        }
155    }
156
157    fn merged(signal: &Signal) -> FanIn<Arm, Select, 2> {
158        FanIn::new(
159            [Arm::Cancel(SignalSource::new(signal)), Arm::Idle],
160            Select::RoundRobin,
161        )
162    }
163
164    fn poll_merge(
165        merge: &FanIn<Arm, Select, 2>,
166        cx: &mut Context<'_>,
167    ) -> Poll<Result<(), Exhausted>> {
168        let mut call = Pipe::call(merge, ());
169        Pin::new(&mut call).poll(cx)
170    }
171
172    #[test]
173    fn fan_in_parked_on_a_signal_source_is_woken_by_fire() {
174        let signal = Signal::new();
175        let merge = merged(&signal);
176        let (counter, waker) = counting_waker();
177        let mut cx = Context::from_waker(&waker);
178        assert_eq!(poll_merge(&merge, &mut cx), Poll::Pending);
179
180        signal.fire();
181
182        assert!(counter.0.load(Ordering::Relaxed) >= 1, "fire must wake the parked fan-in");
183        assert_eq!(poll_merge(&merge, &mut cx), Poll::Ready(Ok(())));
184    }
185
186    #[test]
187    fn dropping_the_source_unregisters_its_waker() {
188        let signal = Signal::new();
189        let (counter, waker) = counting_waker();
190        let mut cx = Context::from_waker(&waker);
191        let source = SignalSource::new(&signal);
192        assert_eq!(call_once(&source, &mut cx), Poll::Pending);
193
194        drop(source);
195        signal.fire();
196
197        assert_eq!(counter.0.load(Ordering::Relaxed), 0);
198    }
199
200    #[test]
201    fn fan_in_over_an_unfired_signal_source_is_never_woken() {
202        let signal = Signal::new();
203        let merge = merged(&signal);
204        let (counter, waker) = counting_waker();
205        let mut cx = Context::from_waker(&waker);
206        for _ in 0..4 {
207            assert_eq!(poll_merge(&merge, &mut cx), Poll::Pending);
208        }
209        assert_eq!(counter.0.load(Ordering::Relaxed), 0);
210    }
211    #[test]
212    fn yields_unit_once_when_fired_then_exhausts() {
213        let signal = Signal::new();
214        let source = SignalSource::new(&signal);
215        let mut cx = noop_context();
216        assert_eq!(call_once(&source, &mut cx), Poll::Pending);
217        signal.fire();
218        assert_eq!(call_once(&source, &mut cx), Poll::Ready(Ok(())));
219        assert_eq!(call_once(&source, &mut cx), Poll::Ready(Err(Exhausted)));
220    }
221
222    #[test]
223    fn late_subscriber_yields_immediately() {
224        let signal = Signal::new();
225        signal.fire();
226        let source = SignalSource::new(&signal);
227        let mut cx = noop_context();
228        assert_eq!(call_once(&source, &mut cx), Poll::Ready(Ok(())));
229    }
230}