Skip to main content

proxima_primitives/pipe/
demand.rs

1//! `Demand<S, G>` — a dormancy gate over any [`SendPipe`].
2//!
3//! Generalises recording's `AccumulatingSink::is_armed()` spigot into a
4//! composable wrapper: when the [`DemandGate`] is closed (no downstream
5//! consumer) the wrapped pipe is a no-op — `call` returns `Ok` immediately with
6//! no await, no inner future, nothing consumed. "No downstream → the pipe is
7//! dormant."
8//!
9//! Dormancy (is there a consumer at all?) is orthogonal to backpressure (is
10//! there room right now? — see [`crate::pipe::BoundedQueue`]). Compose them in series:
11//! `Demand::new(BoundedPipe(inner), gate)`.
12//!
13//! This is a wrapper, NOT a parameter on [`crate::pipe::FanOut`]: a pipe with no gate
14//! pays nothing because it is simply not wrapped, and the gate composes over any
15//! `SendPipe`, not only fans.
16
17use alloc::sync::Arc;
18use core::future::Future;
19use core::sync::atomic::{AtomicBool, Ordering};
20
21use crate::pipe::SendPipe;
22
23/// Whether a [`Demand`]-wrapped pipe currently has downstream demand.
24///
25/// `is_armed` is checked synchronously at `call` entry — no await on the gate.
26/// `AlwaysArmed` folds to a constant `true` the optimiser deletes.
27pub trait DemandGate: Send + Sync + 'static {
28    /// `true` when at least one downstream consumer is active.
29    fn is_armed(&self) -> bool;
30
31    /// Called when the gate transitions armed→disarmed. Default no-op; a gate
32    /// that fronts a [`crate::pipe::BoundedQueue`] overrides this to drain the queue
33    /// and avoid losing items buffered during the coming dormancy.
34    fn on_close(&self) {}
35}
36
37/// The always-on gate: the wrapped pipe always dispatches. Zero-cost — the
38/// `true` check is eliminated by the optimiser.
39pub struct AlwaysArmed;
40
41impl DemandGate for AlwaysArmed {
42    fn is_armed(&self) -> bool {
43        true
44    }
45}
46
47/// A shared atomic gate. Construct via [`AtomicGate::pair`] to get the gate
48/// (handed to the pipe) plus an [`AtomicGateController`] (kept by the caller to
49/// arm/disarm after the gate has been moved into the pipe).
50pub struct AtomicGate(Arc<AtomicBool>);
51
52impl AtomicGate {
53    /// A gate and its controller, sharing one atomic. The gate moves into the
54    /// [`Demand`] wrapper; the controller stays with the caller to signal demand.
55    #[must_use]
56    pub fn pair(initial_armed: bool) -> (Self, AtomicGateController) {
57        let shared = Arc::new(AtomicBool::new(initial_armed));
58        (Self(Arc::clone(&shared)), AtomicGateController(shared))
59    }
60}
61
62impl DemandGate for AtomicGate {
63    fn is_armed(&self) -> bool {
64        self.0.load(Ordering::Acquire)
65    }
66}
67
68/// The write-half of an [`AtomicGate`] — arms/disarms demand from outside the
69/// pipe that holds the gate. A distinct type from the gate so the pipe side
70/// cannot accidentally flip its own demand.
71pub struct AtomicGateController(Arc<AtomicBool>);
72
73impl AtomicGateController {
74    /// Signal that downstream demand exists — the pipe will dispatch.
75    pub fn arm(&self) {
76        self.0.store(true, Ordering::Release);
77    }
78
79    /// Signal that downstream demand is gone — the pipe goes dormant.
80    pub fn disarm(&self) {
81        self.0.store(false, Ordering::Release);
82    }
83
84    #[must_use]
85    pub fn is_armed(&self) -> bool {
86        self.0.load(Ordering::Acquire)
87    }
88}
89
90/// A [`SendPipe`] gated by downstream [`DemandGate`]: dormant (no-op `Ok`) when
91/// the gate is closed, the inner pipe when open.
92pub struct Demand<S, G = AlwaysArmed> {
93    inner: S,
94    gate: G,
95}
96
97impl<S, G> Demand<S, G> {
98    /// Gate `inner` behind `gate`.
99    #[must_use]
100    pub fn new(inner: S, gate: G) -> Self {
101        Self { inner, gate }
102    }
103
104    /// Whether the gate is currently open.
105    #[must_use]
106    pub fn is_armed(&self) -> bool
107    where
108        G: DemandGate,
109    {
110        self.gate.is_armed()
111    }
112}
113
114impl<S, G> SendPipe for Demand<S, G>
115where
116    S: SendPipe,
117    S::In: Send,
118    G: DemandGate,
119{
120    type In = S::In;
121    type Out = ();
122    type Err = S::Err;
123
124    fn call(&self, item: S::In) -> impl Future<Output = Result<(), S::Err>> + Send {
125        // synchronous gate check — a dormant gate consumes nothing and never awaits.
126        let armed = self.gate.is_armed();
127        async move {
128            if armed {
129                self.inner.call(item).await.map(|_| ())
130            } else {
131                Ok(())
132            }
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    #![allow(clippy::unwrap_used, clippy::expect_used)]
140
141    use super::*;
142    use alloc::sync::Arc;
143    use core::sync::atomic::AtomicUsize;
144    use futures::executor::block_on;
145
146    #[derive(Debug)]
147    struct NoErr;
148
149    struct CountingSink(Arc<AtomicUsize>);
150
151    impl SendPipe for CountingSink {
152        type In = u32;
153        type Out = ();
154        type Err = NoErr;
155
156        fn call(&self, _input: u32) -> impl Future<Output = Result<(), NoErr>> + Send {
157            let calls = Arc::clone(&self.0);
158            async move {
159                calls.fetch_add(1, Ordering::Relaxed);
160                Ok(())
161            }
162        }
163    }
164
165    #[test]
166    fn always_armed_always_dispatches() {
167        let calls = Arc::new(AtomicUsize::new(0));
168        let gated = Demand::new(CountingSink(Arc::clone(&calls)), AlwaysArmed);
169        block_on(gated.call(1)).unwrap();
170        assert_eq!(calls.load(Ordering::Relaxed), 1);
171    }
172
173    #[test]
174    fn closed_gate_is_dormant_no_dispatch() {
175        let calls = Arc::new(AtomicUsize::new(0));
176        let (gate, _controller) = AtomicGate::pair(false);
177        let gated = Demand::new(CountingSink(Arc::clone(&calls)), gate);
178        for _ in 0..100 {
179            block_on(gated.call(1)).expect("dormant returns Ok");
180        }
181        assert_eq!(
182            calls.load(Ordering::Relaxed),
183            0,
184            "no dispatch while dormant"
185        );
186    }
187
188    #[test]
189    fn controller_arms_after_gate_moved_into_pipe() {
190        let calls = Arc::new(AtomicUsize::new(0));
191        let (gate, controller) = AtomicGate::pair(false);
192        let gated = Demand::new(CountingSink(Arc::clone(&calls)), gate);
193
194        block_on(gated.call(1)).unwrap();
195        assert_eq!(calls.load(Ordering::Relaxed), 0, "dormant before arm");
196
197        controller.arm();
198        block_on(gated.call(2)).unwrap();
199        assert_eq!(calls.load(Ordering::Relaxed), 1, "dispatches once armed");
200
201        controller.disarm();
202        block_on(gated.call(3)).unwrap();
203        assert_eq!(
204            calls.load(Ordering::Relaxed),
205            1,
206            "dormant again after disarm"
207        );
208    }
209}