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