Skip to main content

proxima_primitives/sync/
semaphore.rs

1//! `proxima::sync::Semaphore` — counted async semaphore, shape-
2//! compatible with `tokio::sync::Semaphore`. Backed by
3//! `async_lock::Semaphore` for permit counting, plus a local
4//! `event_listener::Event` + `AtomicBool` for close-driven cancellation
5//! (which async-lock doesn't expose).
6//!
7//! Acquire-with-close-race semantics: `acquire().await` races the
8//! underlying permit acquire against a close-notification listener.
9//! If the semaphore is closed before the permit lands, the future
10//! resolves to `Err(AcquireError)`. Matches tokio's contract.
11//!
12//! `available_permits()` is tracked locally via an `AtomicUsize`
13//! since async-lock doesn't expose its internal counter. The counter
14//! decrements on `acquire`/`try_acquire` success and increments on
15//! permit drop (via the custom `SemaphorePermit` wrapper).
16//!
17//! # Non-coverage
18//!
19//! - `acquire_many(n)` / `try_acquire_many(n)` — async-lock has no
20//!   atomic N-permit primitive; rolling one requires careful
21//!   retry-with-event-listener atomic logic to preserve the
22//!   "all-or-nothing" guarantee. Out of scope for A2.b; a future
23//!   consumer can promote.
24//! - `acquire_owned()` — owned-permit variant for `Arc<Semaphore>`.
25//!   The Arc-backed path exists on async-lock as `acquire_arc()`;
26//!   shimming an `OwnedSemaphorePermit` over it is straightforward
27//!   but no caller today.
28
29use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
30
31use event_listener::Event;
32use futures::FutureExt;
33
34/// Counted async semaphore.
35#[derive(Debug)]
36pub struct Semaphore {
37    inner: async_lock::Semaphore,
38    closed: AtomicBool,
39    close_event: Event,
40    available: AtomicUsize,
41}
42
43impl Semaphore {
44    /// Build a semaphore with `permits` initial permits.
45    ///
46    /// Under `--cfg loom`, `async_lock::Semaphore::new` and
47    /// `event_listener::Event::new` are deliberately non-`const` (loom's
48    /// model needs a runtime execution context). Only the `const`
49    /// qualifier is affected; the constructed `Semaphore` is identical
50    /// either way. `Semaphore` isn't part of the loom-tested
51    /// Notify/watch protocol, but the crate is built as one unit under
52    /// `--cfg loom`, so this split just keeps it compiling.
53    #[cfg(not(loom))]
54    #[must_use]
55    pub const fn new(permits: usize) -> Self {
56        Self {
57            inner: async_lock::Semaphore::new(permits),
58            closed: AtomicBool::new(false),
59            close_event: Event::new(),
60            available: AtomicUsize::new(permits),
61        }
62    }
63
64    #[cfg(loom)]
65    #[must_use]
66    pub fn new(permits: usize) -> Self {
67        Self {
68            inner: async_lock::Semaphore::new(permits),
69            closed: AtomicBool::new(false),
70            close_event: Event::new(),
71            available: AtomicUsize::new(permits),
72        }
73    }
74
75    /// Acquire one permit, awaiting if necessary. Returns
76    /// `Err(AcquireError)` if the semaphore is (or becomes) closed
77    /// while waiting.
78    pub async fn acquire(&self) -> Result<SemaphorePermit<'_>, AcquireError> {
79        if self.is_closed() {
80            return Err(AcquireError(()));
81        }
82        match self.try_acquire() {
83            Ok(permit) => {
84                if self.is_closed() {
85                    drop(permit);
86                    return Err(AcquireError(()));
87                }
88                return Ok(permit);
89            }
90            Err(TryAcquireError::Closed) => return Err(AcquireError(())),
91            Err(TryAcquireError::NoPermits) => {}
92        }
93        // seqcst preserves event-listener's check/listen/check lost-wake proof.
94        let listener = self.close_event.listen();
95        if self.is_closed() {
96            return Err(AcquireError(()));
97        }
98        let acquire_future = self.inner.acquire().fuse();
99        let mut close_future = listener.fuse();
100        futures::pin_mut!(acquire_future);
101        futures::select_biased! {
102            _ = close_future => Err(AcquireError(())),
103            guard = acquire_future => {
104                self.available.fetch_sub(1, Ordering::Release);
105                Ok(SemaphorePermit { guard: Some(guard), owner: self })
106            }
107        }
108    }
109
110    /// Try to acquire one permit without waiting.
111    pub fn try_acquire(&self) -> Result<SemaphorePermit<'_>, TryAcquireError> {
112        if self.is_closed() {
113            return Err(TryAcquireError::Closed);
114        }
115        match self.inner.try_acquire() {
116            Some(guard) => {
117                self.available.fetch_sub(1, Ordering::Release);
118                Ok(SemaphorePermit {
119                    guard: Some(guard),
120                    owner: self,
121                })
122            }
123            None => Err(TryAcquireError::NoPermits),
124        }
125    }
126
127    /// Inject `n` additional permits into the pool.
128    pub fn add_permits(&self, n: usize) {
129        self.inner.add_permits(n);
130        self.available.fetch_add(n, Ordering::Release);
131    }
132
133    /// Current available permit count. Snapshot only — may change
134    /// concurrently before the caller observes the return value.
135    #[must_use]
136    pub fn available_permits(&self) -> usize {
137        self.available.load(Ordering::Acquire)
138    }
139
140    /// Close the semaphore. Pending and subsequent `acquire`s return
141    /// `Err(AcquireError)`. Idempotent.
142    pub fn close(&self) {
143        self.closed.store(true, Ordering::SeqCst);
144        self.close_event.notify(usize::MAX);
145    }
146
147    /// `true` iff `close()` has been called.
148    #[must_use]
149    pub fn is_closed(&self) -> bool {
150        self.closed.load(Ordering::SeqCst)
151    }
152}
153
154/// Acquired permit. Releases back into the pool on drop.
155#[derive(Debug)]
156pub struct SemaphorePermit<'a> {
157    guard: Option<async_lock::SemaphoreGuard<'a>>,
158    owner: &'a Semaphore,
159}
160
161impl SemaphorePermit<'_> {
162    /// Drop without releasing the permit. Equivalent to tokio's
163    /// `SemaphorePermit::forget`. The available-permits counter is
164    /// not incremented; the permit is "leaked," matching tokio.
165    pub fn forget(mut self) {
166        if let Some(guard) = self.guard.take() {
167            guard.forget();
168        }
169    }
170}
171
172impl Drop for SemaphorePermit<'_> {
173    fn drop(&mut self) {
174        if self.guard.is_some() {
175            self.owner.available.fetch_add(1, Ordering::Release);
176        }
177    }
178}
179
180/// Error returned by `acquire().await` when the semaphore is closed.
181#[derive(Debug, thiserror::Error)]
182#[error("semaphore closed")]
183pub struct AcquireError(());
184
185/// Error returned by `try_acquire()`.
186#[derive(Debug, thiserror::Error)]
187pub enum TryAcquireError {
188    /// Semaphore is closed.
189    #[error("semaphore closed")]
190    Closed,
191    /// No permits currently available.
192    #[error("no permits available")]
193    NoPermits,
194}
195
196// Note: SemaphoreGuard / SemaphoreGuardArc re-exports stay available
197// for callers that need the underlying async-lock types (e.g. when
198// integrating with a third-party crate that takes async-lock guards).
199pub use async_lock::{SemaphoreGuard, SemaphoreGuardArc};
200
201// `#[proxima::test]` and inline `tokio::spawn` pull in the `proxima` /
202// `tokio` dev-dependencies, which the loom build keeps out of the graph
203// (see `[target.'cfg(not(loom))'.dev-dependencies]`); these tests are
204// unrelated to the Notify/watch loom protocol anyway.
205#[cfg(all(test, not(loom)))]
206mod tests {
207    #![allow(
208        clippy::unwrap_used,
209        clippy::expect_used,
210        clippy::field_reassign_with_default,
211        clippy::type_complexity,
212        clippy::useless_vec,
213        clippy::needless_range_loop,
214        clippy::default_constructed_unit_structs
215    )]
216    use alloc::sync::Arc;
217    use alloc::task::Wake;
218    use core::future::Future;
219    use core::task::{Context, Poll, Waker};
220
221    use super::*;
222
223    #[derive(Default)]
224    struct WakeCounter(AtomicUsize);
225
226    impl Wake for WakeCounter {
227        fn wake(self: Arc<Self>) {
228            self.0.fetch_add(1, Ordering::Relaxed);
229        }
230
231        fn wake_by_ref(self: &Arc<Self>) {
232            self.0.fetch_add(1, Ordering::Relaxed);
233        }
234    }
235
236    #[proxima::test]
237    async fn acquire_returns_ok_when_permit_available() {
238        let semaphore = Semaphore::new(2);
239        assert_eq!(semaphore.available_permits(), 2);
240        let permit = semaphore.acquire().await.expect("permit");
241        assert_eq!(semaphore.available_permits(), 1);
242        drop(permit);
243        assert_eq!(semaphore.available_permits(), 2);
244    }
245
246    #[proxima::test]
247    async fn try_acquire_returns_no_permits_when_exhausted() {
248        let semaphore = Semaphore::new(1);
249        let _held = semaphore.acquire().await.expect("first permit");
250        let result = semaphore.try_acquire();
251        assert!(matches!(result, Err(TryAcquireError::NoPermits)));
252    }
253
254    #[proxima::test]
255    async fn close_then_acquire_returns_acquire_error() {
256        let semaphore = Semaphore::new(1);
257        semaphore.close();
258        assert!(semaphore.is_closed());
259        let result = semaphore.acquire().await;
260        assert!(matches!(result, Err(AcquireError(_))));
261    }
262
263    #[proxima::test]
264    async fn close_wakes_pending_acquires() {
265        let semaphore = Semaphore::new(1);
266        let held = semaphore.acquire().await.expect("first permit");
267        let wake_counter = Arc::new(WakeCounter::default());
268        let waker = Waker::from(wake_counter.clone());
269        let mut context = Context::from_waker(&waker);
270        let mut pending_acquire = core::pin::pin!(semaphore.acquire());
271        assert!(matches!(
272            Future::poll(pending_acquire.as_mut(), &mut context),
273            Poll::Pending
274        ));
275        assert_eq!(wake_counter.0.load(Ordering::Relaxed), 0);
276        semaphore.close();
277        assert_eq!(wake_counter.0.load(Ordering::Relaxed), 1);
278        assert!(matches!(
279            Future::poll(pending_acquire.as_mut(), &mut context),
280            Poll::Ready(Err(_))
281        ));
282        wake_counter.0.store(0, Ordering::Relaxed);
283        drop(held);
284        assert_eq!(wake_counter.0.load(Ordering::Relaxed), 0);
285    }
286
287    #[proxima::test]
288    async fn release_wakes_one_pending_acquire() {
289        let semaphore = Semaphore::new(1);
290        let held = semaphore.acquire().await.expect("first permit");
291        let wake_counter = Arc::new(WakeCounter::default());
292        let waker = Waker::from(wake_counter.clone());
293        let mut context = Context::from_waker(&waker);
294        let mut pending_acquire = core::pin::pin!(semaphore.acquire());
295        assert!(matches!(
296            Future::poll(pending_acquire.as_mut(), &mut context),
297            Poll::Pending
298        ));
299        assert_eq!(wake_counter.0.load(Ordering::Relaxed), 0);
300        drop(held);
301        assert_eq!(wake_counter.0.load(Ordering::Relaxed), 1);
302        assert!(matches!(
303            Future::poll(pending_acquire.as_mut(), &mut context),
304            Poll::Ready(Ok(_))
305        ));
306        assert_eq!(semaphore.available_permits(), 1);
307    }
308
309    #[proxima::test]
310    async fn add_permits_increases_available_count() {
311        let semaphore = Semaphore::new(1);
312        semaphore.add_permits(3);
313        assert_eq!(semaphore.available_permits(), 4);
314    }
315}