Skip to main content

proxima_primitives/pipe/
credit_gate.rs

1//! A weighted, awaitable admission stage for bounded Pipe work.
2
3use alloc::sync::Arc;
4use core::fmt::{Debug, Formatter};
5use core::future::Future;
6use core::sync::atomic::{AtomicUsize, Ordering};
7
8use crate::pipe::primitives::Pipe;
9use crate::sync::Notify;
10
11const CLOSED_BIT: usize = 1 << (usize::BITS - 1);
12const CREDIT_MASK: usize = CLOSED_BIT - 1;
13
14/// An input paired with the amount of capacity it needs while in flight.
15pub struct CreditInput<Input> {
16    /// The value passed to the next Pipe stage.
17    pub value: Input,
18    /// The amount of bounded capacity retained until that stage completes.
19    pub credits: usize,
20}
21
22impl<Input> CreditInput<Input> {
23    /// Pair an input with its positive or zero credit weight.
24    #[must_use]
25    pub const fn new(value: Input, credits: usize) -> Self {
26        Self { value, credits }
27    }
28}
29
30/// Pipe stage that awaits weighted capacity and holds it through its inner call.
31pub struct CreditGate<Inner> {
32    inner: Inner,
33    state: Arc<GateState>,
34}
35
36impl<Inner> CreditGate<Inner> {
37    /// Build a gate with a fixed maximum credit capacity.
38    ///
39    /// Capacities must fit in the low bits reserved for the credit counter.
40    pub fn new(inner: Inner, capacity: usize) -> Result<Self, CreditCapacityError> {
41        if capacity > CREDIT_MASK {
42            return Err(CreditCapacityError);
43        }
44        Ok(Self {
45            inner,
46            state: Arc::new(GateState {
47                state: AtomicUsize::new(capacity),
48                capacity,
49                changed: Notify::new(),
50            }),
51        })
52    }
53
54    /// Close admission and wake every caller waiting for capacity.
55    pub fn close(&self) {
56        self.state.state.fetch_or(CLOSED_BIT, Ordering::AcqRel);
57        self.state.changed.notify_waiters();
58    }
59
60    /// Return a snapshot of currently available credits.
61    #[must_use]
62    pub fn available(&self) -> usize {
63        self.state.state.load(Ordering::Acquire) & CREDIT_MASK
64    }
65
66    /// Reserve credits directly for an owned resource that outlives one Pipe call.
67    pub async fn acquire(&self, credits: usize) -> Result<CreditLease, CreditAcquireError> {
68        self.state.acquire(credits).await
69    }
70}
71
72impl<Inner> Pipe for CreditGate<Inner>
73where
74    Inner: Pipe,
75    Inner::In: 'static,
76{
77    type In = CreditInput<Inner::In>;
78    type Out = Inner::Out;
79    type Err = CreditGateError<Self::In, Inner::Err>;
80
81    fn call(&self, input: Self::In) -> impl Future<Output = Result<Self::Out, Self::Err>> {
82        let state = Arc::clone(&self.state);
83        async move {
84            let permit = match state.acquire(input.credits).await {
85                Ok(permit) => permit,
86                Err(CreditAcquireError::Closed) => return Err(CreditGateError::Closed(input)),
87                Err(CreditAcquireError::OverCapacity) => {
88                    return Err(CreditGateError::OverCapacity(input));
89                }
90            };
91            let result = self.inner.call(input.value).await;
92            drop(permit);
93            result.map_err(CreditGateError::Inner)
94        }
95    }
96}
97
98struct GateState {
99    state: AtomicUsize,
100    capacity: usize,
101    changed: Notify,
102}
103
104impl GateState {
105    async fn acquire(self: &Arc<Self>, credits: usize) -> Result<CreditLease, CreditAcquireError> {
106        if credits > self.capacity {
107            return Err(CreditAcquireError::OverCapacity);
108        }
109
110        loop {
111            let notified = self.changed.notified();
112            futures::pin_mut!(notified);
113            let _ = notified.as_mut().enable();
114
115            let current = self.state.load(Ordering::Acquire);
116            if current & CLOSED_BIT != 0 {
117                return Err(CreditAcquireError::Closed);
118            }
119            let available = current & CREDIT_MASK;
120            if available >= credits {
121                match self.state.compare_exchange_weak(
122                    current,
123                    current - credits,
124                    Ordering::AcqRel,
125                    Ordering::Acquire,
126                ) {
127                    Ok(_) => {
128                        return Ok(CreditLease {
129                            state: Arc::clone(self),
130                            credits,
131                        });
132                    }
133                    Err(_) => continue,
134                }
135            }
136
137            notified.await;
138        }
139    }
140}
141
142/// Owned reservation that returns its credits when dropped.
143pub struct CreditLease {
144    state: Arc<GateState>,
145    credits: usize,
146}
147
148impl Drop for CreditLease {
149    fn drop(&mut self) {
150        self.state.state.fetch_add(self.credits, Ordering::Release);
151        self.state.changed.notify_waiters();
152    }
153}
154
155/// Admission failure from a direct [`CreditGate::acquire`] call.
156#[derive(Debug, thiserror::Error)]
157pub enum CreditAcquireError {
158    /// The gate closed before the requested credits were available.
159    #[error("credit gate is closed")]
160    Closed,
161    /// The request exceeds the gate's total capacity.
162    #[error("credit request exceeds gate capacity")]
163    OverCapacity,
164}
165
166/// Error from gate admission or its inner Pipe; admission errors retain input.
167pub enum CreditGateError<Input, InnerError> {
168    /// The gate closed before this input acquired capacity.
169    Closed(Input),
170    /// The input requested more credits than the gate's total capacity.
171    OverCapacity(Input),
172    /// The inner Pipe returned this error after admission.
173    Inner(InnerError),
174}
175
176impl<Input, InnerError: Debug> Debug for CreditGateError<Input, InnerError> {
177    fn fmt(&self, formatter: &mut Formatter<'_>) -> core::fmt::Result {
178        match self {
179            Self::Closed(_) => formatter.write_str("Closed(..)"),
180            Self::OverCapacity(_) => formatter.write_str("OverCapacity(..)"),
181            Self::Inner(error) => formatter.debug_tuple("Inner").field(error).finish(),
182        }
183    }
184}
185
186/// Invalid capacity supplied when constructing a [`CreditGate`].
187#[derive(Debug, thiserror::Error)]
188#[error("credit capacity exceeds the supported counter range")]
189pub struct CreditCapacityError;
190
191#[cfg(test)]
192mod tests {
193    use super::{CreditGate, CreditGateError, CreditInput};
194    use crate::pipe::primitives::Pipe;
195    use core::convert::Infallible;
196    use core::future::Future;
197
198    struct Echo;
199
200    impl Pipe for Echo {
201        type In = usize;
202        type Out = usize;
203        type Err = Infallible;
204
205        fn call(&self, input: usize) -> impl Future<Output = Result<usize, Infallible>> {
206            async move { Ok(input) }
207        }
208    }
209
210    #[proxima::test]
211    async fn test_omega_async_credit_gate_weighted_wait_releases_capacity() {
212        let gate = CreditGate::new(Echo, 3).expect("valid capacity");
213        let held = gate.state.acquire(2).await.expect("first credits");
214        assert_eq!(gate.available(), 1);
215
216        let waiting = gate.call(CreditInput::new(41, 2));
217        futures::pin_mut!(waiting);
218        assert!(matches!(
219            futures::poll!(waiting.as_mut()),
220            core::task::Poll::Pending
221        ));
222        drop(held);
223
224        assert!(matches!(
225            futures::poll!(waiting.as_mut()),
226            core::task::Poll::Ready(Ok(41))
227        ));
228        assert_eq!(gate.available(), 3);
229    }
230
231    #[proxima::test]
232    async fn test_omega_async_credit_gate_never_over_admits() {
233        let gate = CreditGate::new(Echo, 3).expect("valid capacity");
234        let first = gate.state.acquire(2).await.expect("two credits");
235        let second = gate.state.acquire(1).await.expect("last credit");
236        assert_eq!(gate.available(), 0);
237        drop(first);
238        assert_eq!(gate.available(), 2);
239        drop(second);
240        assert_eq!(gate.available(), 3);
241    }
242
243    #[proxima::test]
244    async fn test_omega_async_credit_gate_close_returns_input() {
245        let gate = CreditGate::new(Echo, 1).expect("valid capacity");
246        let held = gate.state.acquire(1).await.expect("held credit");
247        let waiting = gate.call(CreditInput::new(73, 1));
248        futures::pin_mut!(waiting);
249        assert!(matches!(
250            futures::poll!(waiting.as_mut()),
251            core::task::Poll::Pending
252        ));
253        gate.close();
254        assert!(matches!(
255            futures::poll!(waiting.as_mut()),
256            core::task::Poll::Ready(Err(CreditGateError::Closed(CreditInput {
257                value: 73,
258                credits: 1
259            })))
260        ));
261        drop(held);
262    }
263}