monocoque_core/backpressure.rs
1//! Backpressure: `BytePermits`
2//!
3//! Byte-based flow control for write pumps.
4//!
5//! Design principle:
6//! - Backpressure scales with **bytes**, not message count
7//! - One giant message should not starve other connections
8//! - Pluggable: `NoOp` (default) → Semaphore → dynamic policy
9//!
10//! Usage:
11//! ```rust,ignore
12//! let permits = SemaphorePermits::new(10 * 1024 * 1024); // 10MB limit
13//! let permit = permits.acquire(n_bytes).await;
14//! writer.write(buf).await;
15//! drop(permit); // releases automatically
16//! ```
17
18use parking_lot::Mutex;
19use std::collections::VecDeque;
20use std::future::Future;
21use std::pin::Pin;
22use std::sync::Arc;
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::task::{Context, Poll, Waker};
25
26/// Backpressure permit trait.
27///
28/// Implementations control write pump flow based on byte counts.
29///
30/// Native async-fn-in-trait rather than `#[async_trait]`: `acquire` is on the
31/// per-write flow-control path, so the box-per-call async_trait added was pure
32/// overhead. The trait is used behind generics, never as `dyn BytePermits`.
33#[allow(async_fn_in_trait)]
34pub trait BytePermits: Send + Sync {
35 /// Acquire permission to write `n_bytes`.
36 ///
37 /// This may suspend (on the executor, never on a blocking thread) if the
38 /// system is under memory pressure, resuming when enough capacity frees up.
39 async fn acquire(&self, n_bytes: usize) -> Permit;
40}
41
42/// A waiter parked on the byte semaphore.
43///
44/// `granted`/`waker` are mutated only by whoever holds the `SemInner` lock (the
45/// releaser that hands out capacity, or the waiter refreshing its waker), so the
46/// atomic plus a small mutex are all the synchronization the slot needs.
47struct WaiterSlot {
48 /// Bytes this waiter is trying to claim (already clamped to `max_bytes`).
49 needed: usize,
50 /// Set true by the releaser once `needed` bytes have been deducted for this
51 /// waiter. The waiter then converts that reservation into a `Permit`.
52 granted: AtomicBool,
53 /// Waker to notify when granted; refreshed by the waiter on each poll.
54 waker: Mutex<Option<Waker>>,
55}
56
57/// Internal state for the byte semaphore, guarded by one mutex.
58struct SemInner {
59 /// Bytes currently free to hand out.
60 available: usize,
61 /// FIFO queue of parked waiters. Front-to-back granting gives fair ordering
62 /// and bounds head-of-line waiting.
63 waiters: VecDeque<Arc<WaiterSlot>>,
64}
65
66/// Hand freed capacity to the front waiters that now fit, in FIFO order.
67///
68/// Stops at the first waiter that still does not fit, so a single release wakes
69/// only the waiters it can actually satisfy - never a thundering herd. Returns
70/// the wakers to fire; the caller wakes them after dropping the `SemInner` lock.
71fn grant_front(inner: &mut SemInner) -> Vec<Waker> {
72 let mut wakers = Vec::new();
73 loop {
74 let Some(front) = inner.waiters.front() else {
75 break;
76 };
77 if front.needed > inner.available {
78 break;
79 }
80 let slot = inner.waiters.pop_front().expect("front just checked");
81 inner.available -= slot.needed;
82 slot.granted.store(true, Ordering::Release);
83 let waker = slot.waker.lock().take();
84 if let Some(w) = waker {
85 wakers.push(w);
86 }
87 }
88 wakers
89}
90
91/// RAII permit guard.
92///
93/// Releases the permit when dropped.
94pub struct Permit {
95 inner: Option<PermitInner>,
96}
97
98enum PermitInner {
99 /// Byte-counting semaphore claim: returns `n_bytes` to the pool on drop.
100 ByteSem(Arc<Mutex<SemInner>>, usize),
101 NoOp,
102}
103
104impl Drop for Permit {
105 fn drop(&mut self) {
106 if let Some(PermitInner::ByteSem(sem, n_bytes)) = self.inner.take() {
107 // Return the bytes and immediately hand them to any waiters that now
108 // fit. Wake outside the lock to avoid re-entering it from a waker.
109 let wakers = {
110 let mut inner = sem.lock();
111 inner.available += n_bytes;
112 let wakers = grant_front(&mut inner);
113 drop(inner);
114 wakers
115 };
116 for w in wakers {
117 w.wake();
118 }
119 }
120 }
121}
122
123impl Permit {
124 pub(crate) const fn noop() -> Self {
125 Self {
126 inner: Some(PermitInner::NoOp),
127 }
128 }
129
130 fn byte_sem(sem: Arc<Mutex<SemInner>>, n_bytes: usize) -> Self {
131 Self {
132 inner: Some(PermitInner::ByteSem(sem, n_bytes)),
133 }
134 }
135}
136
137/// No-op implementation (Phase 0).
138///
139/// Always grants permits immediately.
140/// Use this until memory pressure becomes an issue.
141#[derive(Debug, Clone, Copy, Default)]
142pub struct NoOpPermits;
143
144impl BytePermits for NoOpPermits {
145 async fn acquire(&self, _n_bytes: usize) -> Permit {
146 Permit::noop()
147 }
148}
149
150/// Semaphore-based backpressure implementation.
151///
152/// Enforces a maximum number of bytes that can be buffered at once. When the
153/// limit is reached, `acquire()` suspends on the executor until enough capacity
154/// is released, then resumes in FIFO order. Acquires all N bytes in a single
155/// atomic operation (O(1), not O(N)).
156///
157/// # Example
158///
159/// ```
160/// use monocoque_core::backpressure::{BytePermits, SemaphorePermits};
161///
162/// # monocoque_core::rt::LocalRuntime::new().unwrap().block_on(async {
163/// // Allow up to 10MB of buffered data
164/// let permits = SemaphorePermits::new(10 * 1024 * 1024);
165///
166/// // Acquire permit for 1KB write
167/// let permit = permits.acquire(1024).await;
168/// // ... perform write ...
169/// drop(permit); // releases 1024 bytes back to the pool
170/// # });
171/// ```
172#[derive(Clone)]
173pub struct SemaphorePermits {
174 inner: Arc<Mutex<SemInner>>,
175 /// Total capacity; oversized acquires clamp to this so they never wait for
176 /// capacity that can never exist.
177 max_bytes: usize,
178}
179
180impl SemaphorePermits {
181 /// Create a new semaphore-based backpressure controller.
182 ///
183 /// # Arguments
184 ///
185 /// * `max_bytes` - Maximum number of bytes that can be buffered
186 #[must_use]
187 pub fn new(max_bytes: usize) -> Self {
188 Self {
189 inner: Arc::new(Mutex::new(SemInner {
190 available: max_bytes,
191 waiters: VecDeque::new(),
192 })),
193 max_bytes,
194 }
195 }
196}
197
198impl BytePermits for SemaphorePermits {
199 async fn acquire(&self, n_bytes: usize) -> Permit {
200 if n_bytes == 0 {
201 return Permit::noop();
202 }
203 // Clamp so a single oversized message consumes the whole pool instead of
204 // waiting forever for capacity that cannot exist.
205 let needed = n_bytes.min(self.max_bytes);
206 Acquire {
207 sem: self.inner.clone(),
208 needed,
209 slot: None,
210 #[cfg(test)]
211 counted_slow: false,
212 }
213 .await
214 }
215}
216
217/// Future returned by `SemaphorePermits::acquire`.
218///
219/// On first poll it claims capacity outright if the pool is free and nobody is
220/// queued ahead of it (preserving FIFO); otherwise it parks a `WaiterSlot` and
221/// completes once the releaser grants it.
222struct Acquire {
223 sem: Arc<Mutex<SemInner>>,
224 needed: usize,
225 slot: Option<Arc<WaiterSlot>>,
226 #[cfg(test)]
227 counted_slow: bool,
228}
229
230impl Future for Acquire {
231 type Output = Permit;
232
233 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Permit> {
234 // Acquire holds no self-references, so it is Unpin and get_mut is sound.
235 let this = self.get_mut();
236
237 // Already parked: complete once the releaser grants us.
238 if let Some(slot) = &this.slot {
239 if slot.granted.load(Ordering::Acquire) {
240 let sem = this.sem.clone();
241 this.slot = None; // taken; Drop must not refund it
242 return Poll::Ready(Permit::byte_sem(sem, this.needed));
243 }
244 // Refresh the waker, then re-check granted to close the race where a
245 // grant lands between the check above and storing the new waker.
246 *slot.waker.lock() = Some(cx.waker().clone());
247 if slot.granted.load(Ordering::Acquire) {
248 let sem = this.sem.clone();
249 this.slot = None;
250 return Poll::Ready(Permit::byte_sem(sem, this.needed));
251 }
252 return Poll::Pending;
253 }
254
255 // First poll: claim immediately only if the pool is free and no one is
256 // waiting ahead of us; otherwise queue to preserve FIFO fairness.
257 let mut inner = this.sem.lock();
258 if inner.waiters.is_empty() && inner.available >= this.needed {
259 inner.available -= this.needed;
260 drop(inner);
261 return Poll::Ready(Permit::byte_sem(this.sem.clone(), this.needed));
262 }
263
264 #[cfg(test)]
265 if !this.counted_slow {
266 SLOW_PATH_ENTRIES.fetch_add(1, Ordering::Relaxed);
267 this.counted_slow = true;
268 }
269
270 let slot = Arc::new(WaiterSlot {
271 needed: this.needed,
272 granted: AtomicBool::new(false),
273 waker: Mutex::new(Some(cx.waker().clone())),
274 });
275 inner.waiters.push_back(slot.clone());
276 drop(inner);
277 this.slot = Some(slot);
278 Poll::Pending
279 }
280}
281
282impl Drop for Acquire {
283 fn drop(&mut self) {
284 let Some(slot) = self.slot.take() else {
285 return;
286 };
287 let wakers = {
288 let mut inner = self.sem.lock();
289 if slot.granted.load(Ordering::Acquire) {
290 // Granted but never turned into a Permit (cancelled between wake
291 // and poll): return the reserved bytes so they can be re-granted.
292 inner.available += self.needed;
293 } else {
294 // Still queued: drop our slot. Removing a large front waiter can
295 // expose smaller ones behind it, which the grant walk then wakes.
296 inner.waiters.retain(|s| !Arc::ptr_eq(s, &slot));
297 }
298 let wakers = grant_front(&mut inner);
299 drop(inner);
300 wakers
301 };
302 for w in wakers {
303 w.wake();
304 }
305 }
306}
307
308/// Counts how many `acquire` calls had to park a waiter (the slow path).
309/// Test-only, used to prove the uncontended fast path claims capacity outright.
310#[cfg(test)]
311static SLOW_PATH_ENTRIES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use std::sync::atomic::Ordering;
317
318 /// `SLOW_PATH_ENTRIES` is a process-global counter, but cargo runs tests in
319 /// parallel, so any test that parks a waiter would pollute a concurrent
320 /// test's reading of it. Every test that touches the counter holds this lock
321 /// for its duration, giving it exclusive use. (Recover from a poisoned lock
322 /// so one failing test does not cascade into the others.)
323 static SLOW_PATH_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
324
325 fn lock_slow_path_counter() -> std::sync::MutexGuard<'static, ()> {
326 SLOW_PATH_TEST_LOCK
327 .lock()
328 .unwrap_or_else(std::sync::PoisonError::into_inner)
329 }
330
331 #[test]
332 fn uncontended_acquire_takes_fast_path() {
333 let _guard = lock_slow_path_counter();
334 // A series of uncontended acquire/release cycles must never park a
335 // waiter, so no queueing overhead is paid on the hot path.
336 let permits = SemaphorePermits::new(1024 * 1024);
337 let rt = crate::rt::LocalRuntime::new().unwrap();
338
339 SLOW_PATH_ENTRIES.store(0, Ordering::Relaxed);
340 rt.block_on(async {
341 for _ in 0..100 {
342 let permit = permits.acquire(1024).await;
343 drop(permit);
344 }
345 });
346 assert_eq!(
347 SLOW_PATH_ENTRIES.load(Ordering::Relaxed),
348 0,
349 "uncontended acquires must not park a waiter"
350 );
351 }
352
353 #[test]
354 fn contended_acquire_parks_then_completes_on_release() {
355 let _guard = lock_slow_path_counter();
356 // With the pool exhausted, a second acquire must park and only complete
357 // once the first permit is released - no blocking thread involved.
358 let permits = SemaphorePermits::new(1024);
359 let rt = crate::rt::LocalRuntime::new().unwrap();
360
361 SLOW_PATH_ENTRIES.store(0, Ordering::Relaxed);
362 rt.block_on(async {
363 let p1 = permits.acquire(1024).await; // exhausts the pool
364
365 let permits2 = permits.clone();
366 let waiter = crate::rt::spawn(async move {
367 // Cannot be satisfied until p1 is released.
368 let _p2 = permits2.acquire(1024).await;
369 });
370
371 // Give the waiter a chance to park, then release.
372 crate::rt::sleep(std::time::Duration::from_millis(50)).await;
373 assert!(
374 SLOW_PATH_ENTRIES.load(Ordering::Relaxed) >= 1,
375 "the second acquire should have parked while the pool was full"
376 );
377 drop(p1);
378 crate::rt::join(waiter).await;
379 });
380 }
381
382 #[test]
383 fn waiters_are_granted_in_fifo_order() {
384 let _guard = lock_slow_path_counter();
385 use std::cell::RefCell;
386 use std::rc::Rc;
387
388 let permits = SemaphorePermits::new(1024);
389 let rt = crate::rt::LocalRuntime::new().unwrap();
390 let order = Rc::new(RefCell::new(Vec::new()));
391
392 rt.block_on(async {
393 let p = permits.acquire(1024).await; // exhaust
394
395 // Queue three waiters in a known order, each needing the full pool.
396 let mut handles = Vec::new();
397 for id in 0..3 {
398 let permits_i = permits.clone();
399 let order_i = order.clone();
400 handles.push(crate::rt::spawn(async move {
401 let _permit = permits_i.acquire(1024).await;
402 order_i.borrow_mut().push(id);
403 // Hold briefly so the next waiter is granted only after this
404 // one releases, making the observed order deterministic.
405 crate::rt::sleep(std::time::Duration::from_millis(10)).await;
406 }));
407 }
408
409 crate::rt::sleep(std::time::Duration::from_millis(50)).await;
410 drop(p); // wakes waiter 0; each release chains to the next
411 for h in handles {
412 crate::rt::join(h).await;
413 }
414 });
415
416 assert_eq!(
417 *order.borrow(),
418 vec![0, 1, 2],
419 "waiters must be granted in the order they queued"
420 );
421 }
422
423 #[test]
424 fn cancelled_waiter_does_not_leak_capacity() {
425 let _guard = lock_slow_path_counter();
426 // A waiter that is polled (parked) and then dropped before being granted
427 // must remove itself so its slot does not wedge the queue, and must not
428 // consume capacity.
429 let permits = SemaphorePermits::new(1024);
430 let rt = crate::rt::LocalRuntime::new().unwrap();
431
432 rt.block_on(async {
433 let p1 = permits.acquire(1024).await; // exhaust
434
435 // Park a waiter, then cancel it.
436 {
437 let mut fut = Box::pin(permits.acquire(512));
438 let polled = futures::poll!(fut.as_mut());
439 assert!(polled.is_pending(), "waiter should park while pool is full");
440 drop(fut); // cancel the parked waiter
441 }
442
443 // Releasing p1 restores the full pool; a fresh acquire of the whole
444 // pool must succeed, proving the cancelled waiter left nothing behind.
445 drop(p1);
446 let _p2 = permits.acquire(1024).await;
447 });
448 }
449
450 #[test]
451 fn noop_permits_always_succeed() {
452 let permits = NoOpPermits;
453 let rt = crate::rt::LocalRuntime::new().unwrap();
454 rt.block_on(async {
455 let _p1 = permits.acquire(1024).await;
456 let _p2 = permits.acquire(1_000_000).await;
457 // Should not block
458 });
459 }
460
461 #[test]
462 fn semaphore_permits_enforce_limit() {
463 let permits = SemaphorePermits::new(1024);
464 let rt = crate::rt::LocalRuntime::new().unwrap();
465
466 rt.block_on(async {
467 // First 1024 bytes should succeed
468 let p1 = permits.acquire(1024).await;
469
470 // Try to acquire more - this would block, so we test the behavior
471 // by checking we can acquire after dropping
472 drop(p1);
473
474 let _p2 = permits.acquire(512).await;
475 let _p3 = permits.acquire(512).await;
476 // Should succeed with 1024 total
477 });
478 }
479
480 #[test]
481 fn semaphore_permits_release_on_drop() {
482 let permits = SemaphorePermits::new(1000);
483 let rt = crate::rt::LocalRuntime::new().unwrap();
484
485 rt.block_on(async {
486 {
487 let _p1 = permits.acquire(500).await;
488 let _p2 = permits.acquire(500).await;
489 // Full capacity used
490 } // Permits dropped here
491
492 // Should be able to acquire again after drop
493 let _p3 = permits.acquire(1000).await;
494 });
495 }
496
497 #[test]
498 fn semaphore_permits_oversized_acquire_does_not_deadlock() {
499 // A single acquire larger than max_bytes must complete (clamped to max_bytes)
500 // rather than deadlocking forever waiting for capacity that can never exist.
501 let permits = SemaphorePermits::new(1024);
502 let rt = crate::rt::LocalRuntime::new().unwrap();
503
504 rt.block_on(async {
505 let permit = permits.acquire(2048).await; // 2× max - must not deadlock
506 drop(permit);
507 // After release, we can acquire up to max_bytes again.
508 let _p = permits.acquire(1024).await;
509 });
510 }
511
512 #[test]
513 fn semaphore_permits_single_atomic_acquire() {
514 // Verify that acquiring N bytes is done atomically (not O(N) individual acquires)
515 let permits = SemaphorePermits::new(1024 * 1024); // 1MB
516 let rt = crate::rt::LocalRuntime::new().unwrap();
517
518 rt.block_on(async {
519 // Acquire a large block in one shot - this should not loop N times
520 let permit = permits.acquire(512 * 1024).await; // 512KB
521 drop(permit);
522 });
523 }
524}