Skip to main content

subetha_cxc/
blocking_semaphore.rs

1//! `BlockingSemaphore`: cross-process counting semaphore with a
2//! kernel-park slow path via [`CrossProcessWaker`].
3//!
4//! Composes [`crate::shared_semaphore::SharedSemaphore`]
5//! (the counter + generation-counter primitive) with one
6//! `CrossProcessWaker`. The hot path is unchanged from
7//! `SharedSemaphore::try_acquire`: a single CAS on the permit
8//! count. The contention slow path differs:
9//!
10//! - **`SharedSemaphore::acquire`** loops `try_acquire` → `yield_now`
11//!   → `sleep(50us)` indefinitely. The sleep tail burns CPU on
12//!   the wake-up tick AND can miss a release by up to 50us.
13//! - **`BlockingSemaphore::acquire_park`** loops `try_acquire`,
14//!   then registers in the waker at the current generation, then
15//!   parks via the platform wait syscall. The kernel returns
16//!   within microseconds of the next `release`.
17//!
18//! Cross-process Linux uses SHARED `futex` so a `release` from
19//! process A wakes a parker in process B. Windows runs intra-
20//! process via `WaitOnAddress` (one process at a time, share via
21//! `Arc::clone`).
22
23use std::path::{Path, PathBuf};
24use std::sync::Arc;
25use std::time::{Duration, Instant};
26
27use crate::cross_process_waker::{
28    CrossProcessWaker, MAX_WAITERS_DEFAULT, WakerError,
29};
30use crate::shared_semaphore::{SemaphoreError, SharedSemaphore};
31
32/// Errors returned by [`BlockingSemaphore`] operations.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum BlockingSemaphoreError {
35    Semaphore(SemaphoreError),
36    Waker(WakerError),
37    Timeout,
38}
39
40impl From<SemaphoreError> for BlockingSemaphoreError {
41    fn from(e: SemaphoreError) -> Self { Self::Semaphore(e) }
42}
43impl From<WakerError> for BlockingSemaphoreError {
44    fn from(e: WakerError) -> Self {
45        match e {
46            WakerError::Timeout => Self::Timeout,
47            other => Self::Waker(other),
48        }
49    }
50}
51
52/// Cross-process semaphore with a kernel-park slow path.
53pub struct BlockingSemaphore {
54    inner: Arc<SharedSemaphore>,
55    waker: Arc<CrossProcessWaker>,
56}
57
58const PRE_PARK_SPIN: u32 = 32;
59
60impl BlockingSemaphore {
61    /// Create a new blocking semaphore. Lays out the underlying
62    /// `SharedSemaphore` files plus a `<base>.waker.bin` for the
63    /// waker. Caller picks `max_permits` (capacity) and
64    /// `init_permits` (starting value); see
65    /// [`SharedSemaphore::create`] for semantics.
66    pub fn create(
67        base_path: impl AsRef<Path>,
68        max_permits: u32,
69        init_permits: u32,
70    ) -> Result<Self, BlockingSemaphoreError> {
71        let base = base_path.as_ref();
72        let inner = SharedSemaphore::create(base, init_permits, max_permits)?;
73        let waker = CrossProcessWaker::create(waker_path(base), MAX_WAITERS_DEFAULT)?;
74        Ok(Self {
75            inner: Arc::new(inner),
76            waker: Arc::new(waker),
77        })
78    }
79
80    /// Open an existing blocking semaphore.
81    pub fn open(
82        base_path: impl AsRef<Path>,
83        expected_max_permits: u32,
84    ) -> Result<Self, BlockingSemaphoreError> {
85        let base = base_path.as_ref();
86        let inner = SharedSemaphore::open(base, expected_max_permits)?;
87        let waker = CrossProcessWaker::open(waker_path(base), MAX_WAITERS_DEFAULT)?;
88        Ok(Self {
89            inner: Arc::new(inner),
90            waker: Arc::new(waker),
91        })
92    }
93
94    /// Non-blocking acquire. Pure CAS; never sleeps.
95    pub fn try_acquire(&self) -> Result<BlockingPermit<'_>, BlockingSemaphoreError> {
96        match self.inner.try_acquire() {
97            Ok(p) => {
98                // The inner Permit's drop would call `release` on the
99                // inner sema; we forget it and re-arm our own Permit
100                // that calls the wrapper's release (which also fires
101                // a wake).
102                std::mem::forget(p);
103                Ok(BlockingPermit { sem: self })
104            }
105            Err(SemaphoreError::WouldBlock) => Err(BlockingSemaphoreError::Semaphore(SemaphoreError::WouldBlock)),
106            Err(e) => Err(BlockingSemaphoreError::Semaphore(e)),
107        }
108    }
109
110    /// Blocking acquire with kernel-park slow path. Returns when a
111    /// permit is available. No timeout variant returns
112    /// `Err(Timeout)`; for a bounded wait use `acquire_park_timeout`.
113    pub fn acquire_park(&self) -> Result<BlockingPermit<'_>, BlockingSemaphoreError> {
114        loop {
115            if let Ok(p) = self.inner.try_acquire() {
116                std::mem::forget(p);
117                return Ok(BlockingPermit { sem: self });
118            }
119            for _ in 0..PRE_PARK_SPIN {
120                if let Ok(p) = self.inner.try_acquire() {
121                    std::mem::forget(p);
122                    return Ok(BlockingPermit { sem: self });
123                }
124                std::hint::spin_loop();
125            }
126            // Slow path: mark as waiter so the inner release path
127            // bumps the wakeup generation; snapshot; double-check;
128            // park.
129            self.inner.mark_waiter_entered();
130            let snapshot = self.inner.wakeup_generation();
131            let token = match self.waker.try_park(snapshot + 1) {
132                Ok(t) => t,
133                Err(e) => {
134                    self.inner.mark_waiter_left();
135                    return Err(BlockingSemaphoreError::from(e));
136                }
137            };
138            if let Ok(p) = self.inner.try_acquire() {
139                self.waker.release(token);
140                self.inner.mark_waiter_left();
141                std::mem::forget(p);
142                return Ok(BlockingPermit { sem: self });
143            }
144            let wait_res = self.waker.wait(token, None);
145            self.inner.mark_waiter_left();
146            wait_res?;
147        }
148    }
149
150    /// Blocking acquire with bounded wait. `Err(Timeout)` when the
151    /// timeout elapses before a permit is available.
152    pub fn acquire_park_timeout(
153        &self,
154        timeout: Duration,
155    ) -> Result<BlockingPermit<'_>, BlockingSemaphoreError> {
156        let deadline = Instant::now() + timeout;
157        loop {
158            if let Ok(p) = self.inner.try_acquire() {
159                std::mem::forget(p);
160                return Ok(BlockingPermit { sem: self });
161            }
162            for _ in 0..PRE_PARK_SPIN {
163                if let Ok(p) = self.inner.try_acquire() {
164                    std::mem::forget(p);
165                    return Ok(BlockingPermit { sem: self });
166                }
167                std::hint::spin_loop();
168            }
169            self.inner.mark_waiter_entered();
170            let snapshot = self.inner.wakeup_generation();
171            let token = match self.waker.try_park(snapshot + 1) {
172                Ok(t) => t,
173                Err(e) => {
174                    self.inner.mark_waiter_left();
175                    return Err(BlockingSemaphoreError::from(e));
176                }
177            };
178            if let Ok(p) = self.inner.try_acquire() {
179                self.waker.release(token);
180                self.inner.mark_waiter_left();
181                std::mem::forget(p);
182                return Ok(BlockingPermit { sem: self });
183            }
184            let now = Instant::now();
185            if now >= deadline {
186                self.waker.release(token);
187                self.inner.mark_waiter_left();
188                return Err(BlockingSemaphoreError::Timeout);
189            }
190            let remaining = deadline - now;
191            let wait_res = self.waker.wait(token, Some(remaining));
192            self.inner.mark_waiter_left();
193            match wait_res {
194                Ok(()) => continue,
195                Err(WakerError::Timeout) => return Err(BlockingSemaphoreError::Timeout),
196                Err(e) => return Err(BlockingSemaphoreError::Waker(e)),
197            }
198        }
199    }
200
201    /// Release one permit. Bumps the inner generation atom and
202    /// fires `wake_up_to(new_gen)` on the waker.
203    pub fn release(&self) -> Result<(), BlockingSemaphoreError> {
204        self.inner.release()?;
205        // SharedSemaphore::release internally fetched-add'd wakeup
206        // when waiters > 0. The wake on our side is unconditional
207        // (cheap if no slots parked).
208        let new_gen = self.inner.wakeup_generation();
209        self.waker.wake_up_to(new_gen);
210        Ok(())
211    }
212
213    /// Available-permits observation (may race).
214    pub fn available(&self) -> u32 { self.inner.available() }
215
216    /// Cap fixed at construction.
217    pub fn max_permits(&self) -> u32 { self.inner.max_permits() }
218
219    /// Inner primitive (for sidecar / observability hooks).
220    pub fn inner(&self) -> &Arc<SharedSemaphore> { &self.inner }
221}
222
223/// RAII guard for an acquired permit. Drops to `release`.
224pub struct BlockingPermit<'a> {
225    sem: &'a BlockingSemaphore,
226}
227
228impl Drop for BlockingPermit<'_> {
229    fn drop(&mut self) {
230        // Release-overflow + waker errors are unrecoverable from
231        // inside Drop; surface them on stderr and continue so the
232        // RAII chain still runs.
233        if let Err(e) = self.sem.release() {
234            eprintln!("BlockingSemaphore: release failed in Drop: {e:?}");
235        }
236    }
237}
238
239fn waker_path(base: &Path) -> PathBuf {
240    let mut p = base.as_os_str().to_owned();
241    p.push(".waker.bin");
242    PathBuf::from(p)
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use std::sync::atomic::{AtomicU64, Ordering};
249    use std::thread;
250
251    fn fresh_base() -> PathBuf {
252        let dir = std::env::temp_dir();
253        // Use both pid and a per-test counter so parallel tests in
254        // the same process don't clobber each other.
255        static N: AtomicU64 = AtomicU64::new(0);
256        let n = N.fetch_add(1, Ordering::Relaxed);
257        dir.join(format!("subetha_bsem_test_{}_{}", std::process::id(), n))
258    }
259
260    fn cleanup(base: &Path) {
261        for suffix in [
262            ".count.bin", ".wakeup.bin", ".waiters.bin",
263            ".count.bin.hh.bin", ".count.bin.ring.bin",
264            ".wakeup.bin.hh.bin", ".wakeup.bin.ring.bin",
265            ".waiters.bin.hh.bin", ".waiters.bin.ring.bin",
266            ".hh.bin", ".ring.bin",
267            ".waker.bin",
268        ] {
269            let mut p = base.as_os_str().to_owned();
270            p.push(suffix);
271            drop(std::fs::remove_file(PathBuf::from(p)));
272        }
273    }
274
275    #[test]
276    fn try_acquire_succeeds_when_permits_available() {
277        let base = fresh_base();
278        cleanup(&base);
279        let sem = BlockingSemaphore::create(&base, 4, 4).expect("create");
280        let p = sem.try_acquire().expect("permit");
281        drop(p);
282        cleanup(&base);
283    }
284
285    #[test]
286    fn acquire_park_blocks_then_completes_on_release() {
287        let base = fresh_base();
288        cleanup(&base);
289        let sem = Arc::new(BlockingSemaphore::create(&base, 1, 1).expect("create"));
290        let p0 = sem.try_acquire().expect("permit-0");
291
292        // Assert the ORDERING property directly: the parked
293        // acquirer cannot complete before the permit's release. (A
294        // fixed sleep + minimum-elapsed assertion is schedule-
295        // sensitive: under full-suite load the spawned thread can
296        // start late and measure a short block despite behaving
297        // correctly.)
298        let s2 = Arc::clone(&sem);
299        let t = thread::spawn(move || {
300            let _g = s2.acquire_park().expect("park-acquire");
301            Instant::now()
302        });
303        thread::sleep(Duration::from_millis(40));
304        let released_at = Instant::now();
305        drop(p0); // release fires wake_up_to.
306        let completed_at = t.join().unwrap();
307        assert!(completed_at >= released_at,
308                "acquire_park must not complete before the permit released");
309
310        cleanup(&base);
311    }
312
313    #[test]
314    fn acquire_park_timeout_returns_timeout() {
315        let base = fresh_base();
316        cleanup(&base);
317        let sem = BlockingSemaphore::create(&base, 1, 1).expect("create");
318        let _hold = sem.try_acquire().expect("hold");
319        let t0 = Instant::now();
320        let err = sem.acquire_park_timeout(Duration::from_millis(60));
321        assert!(matches!(err, Err(BlockingSemaphoreError::Timeout)));
322        assert!(t0.elapsed() >= Duration::from_millis(50));
323        cleanup(&base);
324    }
325}