proxima_primitives/pipe/
credit_gate.rs1use 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
14pub struct CreditInput<Input> {
16 pub value: Input,
18 pub credits: usize,
20}
21
22impl<Input> CreditInput<Input> {
23 #[must_use]
25 pub const fn new(value: Input, credits: usize) -> Self {
26 Self { value, credits }
27 }
28}
29
30pub struct CreditGate<Inner> {
32 inner: Inner,
33 state: Arc<GateState>,
34}
35
36impl<Inner> CreditGate<Inner> {
37 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 pub fn close(&self) {
56 self.state.state.fetch_or(CLOSED_BIT, Ordering::AcqRel);
57 self.state.changed.notify_waiters();
58 }
59
60 #[must_use]
62 pub fn available(&self) -> usize {
63 self.state.state.load(Ordering::Acquire) & CREDIT_MASK
64 }
65
66 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
142pub 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#[derive(Debug, thiserror::Error)]
157pub enum CreditAcquireError {
158 #[error("credit gate is closed")]
160 Closed,
161 #[error("credit request exceeds gate capacity")]
163 OverCapacity,
164}
165
166pub enum CreditGateError<Input, InnerError> {
168 Closed(Input),
170 OverCapacity(Input),
172 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#[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}