Skip to main content

execution_policy/
concurrency.rs

1//! Bounded concurrency: a runtime-agnostic async semaphore (no tokio dependency)
2//! plus the explicit saturation policy. Permits release on drop and wake the
3//! next waiter.
4
5use std::collections::VecDeque;
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::{Arc, Mutex};
9use std::task::{Context, Poll, Waker};
10use std::time::Duration;
11
12/// What happens when all permits are taken.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum SaturationPolicy {
15    /// Queue up to `max_queued` callers, each waiting at most `queue_timeout`.
16    Wait {
17        max_queued: usize,
18        queue_timeout: Option<Duration>,
19    },
20    /// Reject immediately (load-shed).
21    Reject,
22}
23
24/// Whether the limit counts whole operations or individual attempts.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub(crate) enum Scope {
27    Operations,
28    Attempts,
29}
30
31/// Bounded-concurrency configuration.
32#[derive(Debug, Clone)]
33pub struct ConcurrencyLimit {
34    permits: usize,
35    scope: Scope,
36    saturation: SaturationPolicy,
37}
38
39impl ConcurrencyLimit {
40    /// Limit concurrent *operations* (one permit per top-level call).
41    pub fn operations(n: usize) -> Self {
42        Self::new(n, Scope::Operations)
43    }
44    /// Limit concurrent *attempts* (one permit per attempt, retries included).
45    pub fn attempts(n: usize) -> Self {
46        Self::new(n, Scope::Attempts)
47    }
48
49    fn new(permits: usize, scope: Scope) -> Self {
50        Self {
51            permits: permits.max(1),
52            scope,
53            saturation: SaturationPolicy::Wait {
54                max_queued: usize::MAX,
55                queue_timeout: None,
56            },
57        }
58    }
59
60    /// Cap the number of waiting callers (excess is rejected).
61    pub fn max_queued(mut self, n: usize) -> Self {
62        self.saturation = match self.saturation {
63            SaturationPolicy::Wait { queue_timeout, .. } => SaturationPolicy::Wait {
64                max_queued: n,
65                queue_timeout,
66            },
67            SaturationPolicy::Reject => SaturationPolicy::Reject,
68        };
69        self
70    }
71    /// Maximum time a caller waits in the queue before being rejected.
72    pub fn queue_timeout(mut self, d: Duration) -> Self {
73        self.saturation = match self.saturation {
74            SaturationPolicy::Wait { max_queued, .. } => SaturationPolicy::Wait {
75                max_queued,
76                queue_timeout: Some(d),
77            },
78            SaturationPolicy::Reject => SaturationPolicy::Reject,
79        };
80        self
81    }
82    /// Reject immediately when saturated instead of queueing.
83    pub fn reject(mut self) -> Self {
84        self.saturation = SaturationPolicy::Reject;
85        self
86    }
87
88    pub(crate) fn build(&self) -> Arc<Semaphore> {
89        Semaphore::new(self.permits)
90    }
91
92    /// Compile into the live shared semaphore + policy used by the engine.
93    pub(crate) fn compile(&self) -> CompiledConcurrency {
94        CompiledConcurrency {
95            sem: self.build(),
96            saturation: self.saturation,
97            scope: self.scope,
98        }
99    }
100}
101
102/// Live, shared concurrency gate stored in the compiled `Plan`.
103#[derive(Debug)]
104pub(crate) struct CompiledConcurrency {
105    pub(crate) sem: Arc<Semaphore>,
106    pub(crate) saturation: SaturationPolicy,
107    pub(crate) scope: Scope,
108}
109
110impl From<usize> for ConcurrencyLimit {
111    /// `n` is shorthand for `ConcurrencyLimit::operations(n)`.
112    fn from(n: usize) -> Self {
113        ConcurrencyLimit::operations(n)
114    }
115}
116
117impl From<u32> for ConcurrencyLimit {
118    fn from(n: u32) -> Self {
119        ConcurrencyLimit::operations(n as usize)
120    }
121}
122
123#[derive(Debug)]
124struct SemState {
125    permits: usize,
126    waiters: VecDeque<Waker>,
127}
128
129/// A runtime-agnostic counting semaphore.
130#[derive(Debug)]
131pub(crate) struct Semaphore {
132    state: Mutex<SemState>,
133}
134
135impl Semaphore {
136    fn new(permits: usize) -> Arc<Self> {
137        Arc::new(Self {
138            state: Mutex::new(SemState {
139                permits,
140                waiters: VecDeque::new(),
141            }),
142        })
143    }
144
145    /// Number of callers currently queued.
146    pub(crate) fn queued(self: &Arc<Self>) -> usize {
147        self.state.lock().unwrap().waiters.len()
148    }
149
150    /// Try to take a permit without waiting.
151    pub(crate) fn try_acquire(self: &Arc<Self>) -> Option<Permit> {
152        let mut st = self.state.lock().unwrap();
153        if st.permits > 0 {
154            st.permits -= 1;
155            Some(Permit {
156                sem: Arc::clone(self),
157            })
158        } else {
159            None
160        }
161    }
162
163    /// Wait for a permit.
164    pub(crate) fn acquire(self: &Arc<Self>) -> Acquire {
165        Acquire {
166            sem: Arc::clone(self),
167        }
168    }
169
170    fn release(&self) {
171        let mut st = self.state.lock().unwrap();
172        st.permits += 1;
173        if let Some(w) = st.waiters.pop_front() {
174            w.wake();
175        }
176    }
177}
178
179/// A held concurrency permit; releases on drop.
180#[derive(Debug)]
181pub(crate) struct Permit {
182    sem: Arc<Semaphore>,
183}
184
185impl Drop for Permit {
186    fn drop(&mut self) {
187        self.sem.release();
188    }
189}
190
191/// Future returned by [`Semaphore::acquire`].
192pub(crate) struct Acquire {
193    sem: Arc<Semaphore>,
194}
195
196impl Future for Acquire {
197    type Output = Permit;
198
199    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Permit> {
200        let mut st = self.sem.state.lock().unwrap();
201        if st.permits > 0 {
202            st.permits -= 1;
203            return Poll::Ready(Permit {
204                sem: Arc::clone(&self.sem),
205            });
206        }
207        // Register our waker to be woken on the next release.
208        st.waiters.push_back(cx.waker().clone());
209        Poll::Pending
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[tokio::test]
218    async fn permit_release_lets_next_in() {
219        let cfg = ConcurrencyLimit::operations(1);
220        let sem = cfg.build();
221        let p1 = sem.try_acquire().expect("first permit");
222        assert!(sem.try_acquire().is_none(), "saturated");
223
224        // Acquire should pend until p1 drops.
225        let acq = sem.acquire();
226        tokio::pin!(acq);
227        let waiter = async { (&mut acq).await };
228        tokio::pin!(waiter);
229
230        let releaser = async {
231            tokio::task::yield_now().await;
232            drop(p1); // releases → wakes the waiter
233        };
234        let (_p2, ()) = tokio::join!(waiter, releaser);
235        // _p2 now holds the only permit.
236        assert!(sem.try_acquire().is_none());
237    }
238
239    #[test]
240    fn queued_counts_waiters() {
241        let sem = ConcurrencyLimit::operations(1).build();
242        let _p = sem.try_acquire().unwrap();
243        assert_eq!(sem.queued(), 0);
244    }
245
246    #[test]
247    fn saturation_builders() {
248        let c = ConcurrencyLimit::attempts(8)
249            .max_queued(4)
250            .queue_timeout(Duration::from_millis(10))
251            .compile();
252        assert_eq!(c.scope, Scope::Attempts);
253        match c.saturation {
254            SaturationPolicy::Wait {
255                max_queued,
256                queue_timeout,
257            } => {
258                assert_eq!(max_queued, 4);
259                assert_eq!(queue_timeout, Some(Duration::from_millis(10)));
260            }
261            _ => panic!("expected Wait"),
262        }
263        assert_eq!(
264            ConcurrencyLimit::operations(2)
265                .reject()
266                .compile()
267                .saturation,
268            SaturationPolicy::Reject
269        );
270    }
271}