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 async_trait::async_trait;
19use parking_lot::{Condvar, Mutex};
20use std::sync::Arc;
21
22/// Backpressure permit trait.
23///
24/// Implementations control write pump flow based on byte counts.
25#[async_trait]
26pub trait BytePermits: Send + Sync {
27    /// Acquire permission to write `n_bytes`.
28    ///
29    /// This may block if the system is under memory pressure.
30    async fn acquire(&self, n_bytes: usize) -> Permit;
31}
32
33/// Internal state for the byte semaphore.
34struct SemInner {
35    available: usize,
36    /// Total capacity; used to clamp oversized acquires so we never deadlock.
37    max_bytes: usize,
38}
39
40/// RAII permit guard.
41///
42/// Releases the permit when dropped.
43pub struct Permit {
44    inner: Option<PermitInner>,
45}
46
47enum PermitInner {
48    /// Byte-counting semaphore backed by `parking_lot` primitives (usable in `Drop`).
49    ByteSem(Arc<(Mutex<SemInner>, Condvar)>, usize),
50    NoOp,
51}
52
53impl Drop for Permit {
54    fn drop(&mut self) {
55        match self.inner.take() {
56            Some(PermitInner::ByteSem(inner, n_bytes)) => {
57                let (mutex, condvar) = &*inner;
58                let mut guard = mutex.lock();
59                guard.available += n_bytes;
60                drop(guard);
61                condvar.notify_all();
62            }
63            Some(PermitInner::NoOp) | None => {}
64        }
65    }
66}
67
68impl Permit {
69    pub(crate) const fn noop() -> Self {
70        Self {
71            inner: Some(PermitInner::NoOp),
72        }
73    }
74
75    fn byte_sem(inner: Arc<(Mutex<SemInner>, Condvar)>, n_bytes: usize) -> Self {
76        Self {
77            inner: Some(PermitInner::ByteSem(inner, n_bytes)),
78        }
79    }
80}
81
82/// No-op implementation (Phase 0).
83///
84/// Always grants permits immediately.
85/// Use this until memory pressure becomes an issue.
86#[derive(Debug, Clone, Copy, Default)]
87pub struct NoOpPermits;
88
89#[async_trait]
90impl BytePermits for NoOpPermits {
91    async fn acquire(&self, _n_bytes: usize) -> Permit {
92        Permit::noop()
93    }
94}
95
96/// Semaphore-based backpressure implementation.
97///
98/// Enforces a maximum number of bytes that can be buffered at once.
99/// When the limit is reached, `acquire()` will block until space is available.
100/// Acquires all N bytes in a single atomic operation (O(1), not O(N)).
101///
102/// # Example
103///
104/// ```
105/// use monocoque_core::backpressure::{BytePermits, SemaphorePermits};
106///
107/// # monocoque_core::rt::LocalRuntime::new().unwrap().block_on(async {
108/// // Allow up to 10MB of buffered data
109/// let permits = SemaphorePermits::new(10 * 1024 * 1024);
110///
111/// // Acquire permit for 1KB write
112/// let permit = permits.acquire(1024).await;
113/// // ... perform write ...
114/// drop(permit); // releases 1024 bytes back to the pool
115/// # });
116/// ```
117#[derive(Clone)]
118pub struct SemaphorePermits {
119    inner: Arc<(Mutex<SemInner>, Condvar)>,
120}
121
122impl SemaphorePermits {
123    /// Create a new semaphore-based backpressure controller.
124    ///
125    /// # Arguments
126    ///
127    /// * `max_bytes` - Maximum number of bytes that can be buffered
128    #[must_use]
129    pub fn new(max_bytes: usize) -> Self {
130        Self {
131            inner: Arc::new((
132                Mutex::new(SemInner {
133                    available: max_bytes,
134                    max_bytes,
135                }),
136                Condvar::new(),
137            )),
138        }
139    }
140}
141
142#[async_trait]
143impl BytePermits for SemaphorePermits {
144    async fn acquire(&self, n_bytes: usize) -> Permit {
145        if n_bytes == 0 {
146            return Permit::noop();
147        }
148
149        // Fast path: in the uncontended case capacity is available and an
150        // in-place claim under a non-blocking try_lock is all that is needed.
151        // This stays on the executor and avoids a thread-pool round trip per
152        // write, which otherwise dominates the latency of a high-rate write
153        // path. try_lock never parks the executor thread. The mutex remains the
154        // single source of truth, so this cannot race the slow path below.
155        {
156            let (mutex, _condvar) = &*self.inner;
157            if let Some(mut guard) = mutex.try_lock() {
158                let claim = n_bytes.min(guard.max_bytes);
159                if guard.available >= claim {
160                    guard.available -= claim;
161                    drop(guard);
162                    return Permit::byte_sem(self.inner.clone(), claim);
163                }
164            }
165        }
166
167        // Slow path: contended, or not enough capacity right now. Wait on a
168        // dedicated thread so we don't block the async executor.
169        // parking_lot::Condvar::wait is synchronous and safe to use here
170        // because SemInner uses parking_lot::Mutex.
171        #[cfg(test)]
172        SLOW_PATH_ENTRIES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
173        let inner = self.inner.clone();
174        let actual = crate::rt::spawn_blocking(move || {
175            let (mutex, condvar) = &*inner;
176            let mut guard = mutex.lock();
177            // Clamp to max_bytes so a single oversized message never deadlocks:
178            // the message will consume the entire capacity instead of waiting
179            // forever for capacity that can never exist.
180            let claim = n_bytes.min(guard.max_bytes);
181            // Wait until enough capacity is available.
182            while guard.available < claim {
183                condvar.wait(&mut guard);
184            }
185            guard.available -= claim;
186            // Return the actual bytes claimed so the Permit releases the right amount.
187            claim
188        })
189        .await;
190
191        Permit::byte_sem(self.inner.clone(), actual)
192    }
193}
194
195/// Counts how many `acquire` calls fell through to the blocking slow path.
196/// Test-only, used to prove the uncontended fast path avoids `spawn_blocking`.
197#[cfg(test)]
198static SLOW_PATH_ENTRIES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use std::sync::atomic::Ordering;
204
205    #[test]
206    fn uncontended_acquire_takes_fast_path() {
207        // A series of uncontended acquire/release cycles must never enter the
208        // blocking slow path, so no per-write thread-pool round trip is paid.
209        let permits = SemaphorePermits::new(1024 * 1024);
210        let rt = crate::rt::LocalRuntime::new().unwrap();
211
212        SLOW_PATH_ENTRIES.store(0, Ordering::Relaxed);
213        rt.block_on(async {
214            for _ in 0..100 {
215                let permit = permits.acquire(1024).await;
216                drop(permit);
217            }
218        });
219        assert_eq!(
220            SLOW_PATH_ENTRIES.load(Ordering::Relaxed),
221            0,
222            "uncontended acquires must not hit the spawn_blocking slow path"
223        );
224    }
225
226    #[test]
227    fn insufficient_capacity_uses_slow_path() {
228        // When capacity is exhausted the acquire must fall back to the blocking
229        // wait path (and complete once capacity is released).
230        let permits = SemaphorePermits::new(1024);
231        let rt = crate::rt::LocalRuntime::new().unwrap();
232
233        SLOW_PATH_ENTRIES.store(0, Ordering::Relaxed);
234        rt.block_on(async {
235            let p1 = permits.acquire(1024).await; // fast path, exhausts capacity
236            // This one cannot be satisfied immediately; release then reacquire.
237            drop(p1);
238            let _p2 = permits.acquire(1024).await;
239        });
240        // The first acquire took the fast path; only note that the mechanism is
241        // exercised without deadlock. (Exact slow-path count is timing
242        // dependent because the drop may free capacity before the retry.)
243    }
244
245    #[test]
246    fn noop_permits_always_succeed() {
247        let permits = NoOpPermits;
248        let rt = crate::rt::LocalRuntime::new().unwrap();
249        rt.block_on(async {
250            let _p1 = permits.acquire(1024).await;
251            let _p2 = permits.acquire(1_000_000).await;
252            // Should not block
253        });
254    }
255
256    #[test]
257    fn semaphore_permits_enforce_limit() {
258        let permits = SemaphorePermits::new(1024);
259        let rt = crate::rt::LocalRuntime::new().unwrap();
260
261        rt.block_on(async {
262            // First 1024 bytes should succeed
263            let p1 = permits.acquire(1024).await;
264
265            // Try to acquire more - this would block, so we test the behavior
266            // by checking we can acquire after dropping
267            drop(p1);
268
269            let _p2 = permits.acquire(512).await;
270            let _p3 = permits.acquire(512).await;
271            // Should succeed with 1024 total
272        });
273    }
274
275    #[test]
276    fn semaphore_permits_release_on_drop() {
277        let permits = SemaphorePermits::new(1000);
278        let rt = crate::rt::LocalRuntime::new().unwrap();
279
280        rt.block_on(async {
281            {
282                let _p1 = permits.acquire(500).await;
283                let _p2 = permits.acquire(500).await;
284                // Full capacity used
285            } // Permits dropped here
286
287            // Should be able to acquire again after drop
288            let _p3 = permits.acquire(1000).await;
289        });
290    }
291
292    #[test]
293    fn semaphore_permits_oversized_acquire_does_not_deadlock() {
294        // A single acquire larger than max_bytes must complete (clamped to max_bytes)
295        // rather than deadlocking forever waiting for capacity that can never exist.
296        let permits = SemaphorePermits::new(1024);
297        let rt = crate::rt::LocalRuntime::new().unwrap();
298
299        rt.block_on(async {
300            let permit = permits.acquire(2048).await; // 2× max - must not deadlock
301            drop(permit);
302            // After release, we can acquire up to max_bytes again.
303            let _p = permits.acquire(1024).await;
304        });
305    }
306
307    #[test]
308    fn semaphore_permits_single_atomic_acquire() {
309        // Verify that acquiring N bytes is done atomically (not O(N) individual acquires)
310        let permits = SemaphorePermits::new(1024 * 1024); // 1MB
311        let rt = crate::rt::LocalRuntime::new().unwrap();
312
313        rt.block_on(async {
314            // Acquire a large block in one shot - this should not loop N times
315            let permit = permits.acquire(512 * 1024).await; // 512KB
316            drop(permit);
317        });
318    }
319}