Skip to main content

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}