Skip to main content

reddb_server/server/
http_principal_limiter.rs

1//! Per-principal in-flight admission for the async HTTP edge (issue #934,
2//! PRD #930).
3//!
4//! The thread-per-connection cap retired in #931 used to bound *both*
5//! total concurrency and any single caller's share of it as a side effect
6//! — one greedy client could only ever hold as many slots as it held OS
7//! threads, and the global `(2*num_cpus).clamp(8,256)` ceiling capped
8//! that. The async edge removed the OS-thread coupling: an idle keep-alive
9//! connection is now a parked future, and admission is per in-flight
10//! request against a single global [`HttpConnectionLimiter`]. That global
11//! cap bounds *total* in-flight work (async backpressure) but no longer
12//! bounds any single principal's share — one abusive caller can drain the
13//! whole global cap and starve everyone else.
14//!
15//! This limiter restores the per-caller bound as a first-class control: a
16//! small `AtomicUsize`-per-principal in-flight counter consulted at the
17//! edge *after* global admission. A principal over its own cap gets a
18//! structured 429 refusal (see [`PrincipalCapExceeded`]) carrying the
19//! limit, the live count, and the principal label so a well-behaved client
20//! can back off without guessing. It is the concurrency sibling of the
21//! per-principal QPS quota ([`crate::runtime::quota_bucket::QuotaBucket`],
22//! which bounds *rate*) and the per-principal stream cap (ADR 0029 /
23//! `StreamCapacityRegistry`, which bounds concurrent *streams*).
24//!
25//! A `cap` of `0` disables the limiter entirely (every acquire succeeds) so
26//! operators can opt out and so small single-tenant deployments pay nothing.
27
28use std::collections::HashMap;
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::sync::{Arc, Mutex};
31
32/// Refusal returned by [`PrincipalConnectionLimiter::try_acquire`] when a
33/// principal is already at its concurrent in-flight cap. Carries the values
34/// a client needs to back off intelligently: its own cap, the live count it
35/// hit, and the principal label the server bucketed it under.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct PrincipalCapExceeded {
38    pub principal: String,
39    pub limit: usize,
40    pub current: usize,
41}
42
43/// Stable machine-readable refusal code embedded in the structured body so
44/// clients branch on it without parsing prose.
45pub const PRINCIPAL_INFLIGHT_CODE: &str = "principal_inflight_exhausted";
46
47#[derive(Debug)]
48struct Inner {
49    /// Per-principal concurrent in-flight ceiling. `0` disables the
50    /// limiter (every acquire succeeds, no map mutation).
51    cap: usize,
52    /// `principal -> live in-flight count`. An entry exists only while a
53    /// principal holds at least one permit; it is removed when its count
54    /// returns to zero so the map can't grow unbounded under churn.
55    in_use: Mutex<HashMap<String, usize>>,
56    /// Total refusals since process start, surfaced to `/metrics`.
57    rejected: AtomicU64,
58}
59
60/// Permit handle — owns one in-flight slot for a single principal. Dropping
61/// the permit decrements that principal's count (and evicts the entry at
62/// zero). Intentionally `!Clone` so slot accounting can't drift.
63#[derive(Debug)]
64pub struct PrincipalInflightPermit {
65    inner: Arc<Inner>,
66    principal: String,
67}
68
69impl Drop for PrincipalInflightPermit {
70    fn drop(&mut self) {
71        let mut map = self.inner.in_use.lock().expect("principal limiter mutex");
72        if let Some(count) = map.get_mut(&self.principal) {
73            *count -= 1;
74            if *count == 0 {
75                map.remove(&self.principal);
76            }
77        }
78    }
79}
80
81#[derive(Debug, Clone)]
82pub struct PrincipalConnectionLimiter {
83    inner: Arc<Inner>,
84}
85
86impl PrincipalConnectionLimiter {
87    /// Build a limiter with the given per-principal concurrent in-flight
88    /// cap. `0` disables enforcement.
89    pub fn new(cap: usize) -> Self {
90        Self {
91            inner: Arc::new(Inner {
92                cap,
93                in_use: Mutex::new(HashMap::new()),
94                rejected: AtomicU64::new(0),
95            }),
96        }
97    }
98
99    /// The configured per-principal cap. `0` means disabled.
100    pub fn cap(&self) -> usize {
101        self.inner.cap
102    }
103
104    /// `true` when enforcement is active (cap > 0).
105    pub fn is_enforced(&self) -> bool {
106        self.inner.cap > 0
107    }
108
109    /// Live in-flight count for a single principal (`0` if it holds none).
110    /// Observability only.
111    pub fn current_for(&self, principal: &str) -> usize {
112        let map = self.inner.in_use.lock().expect("principal limiter mutex");
113        map.get(principal).copied().unwrap_or(0)
114    }
115
116    /// Number of principals currently holding at least one permit.
117    pub fn tracked_principals(&self) -> usize {
118        let map = self.inner.in_use.lock().expect("principal limiter mutex");
119        map.len()
120    }
121
122    /// Total refusals issued since construction. Surfaced via
123    /// `reddb_http_principal_inflight_rejected_total`.
124    pub fn rejected_total(&self) -> u64 {
125        self.inner.rejected.load(Ordering::Relaxed)
126    }
127
128    /// Admit one in-flight request for `principal`. Returns `Ok(permit)`
129    /// when the principal is under its cap (slot reserved, released on
130    /// permit drop) or `Err(PrincipalCapExceeded)` when it is already at
131    /// the cap. A disabled limiter (cap `0`) always admits and never
132    /// touches the map.
133    pub fn try_acquire(
134        &self,
135        principal: &str,
136    ) -> Result<PrincipalInflightPermit, PrincipalCapExceeded> {
137        if self.inner.cap == 0 {
138            return Ok(PrincipalInflightPermit {
139                inner: Arc::clone(&self.inner),
140                principal: principal.to_string(),
141            });
142        }
143        let mut map = self.inner.in_use.lock().expect("principal limiter mutex");
144        let count = map.entry(principal.to_string()).or_insert(0);
145        if *count >= self.inner.cap {
146            let current = *count;
147            // Drop the lock before bumping the (Relaxed) counter — keep the
148            // critical section to the map mutation only.
149            drop(map);
150            self.inner.rejected.fetch_add(1, Ordering::Relaxed);
151            return Err(PrincipalCapExceeded {
152                principal: principal.to_string(),
153                limit: self.inner.cap,
154                current,
155            });
156        }
157        *count += 1;
158        Ok(PrincipalInflightPermit {
159            inner: Arc::clone(&self.inner),
160            principal: principal.to_string(),
161        })
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use std::sync::atomic::AtomicUsize;
169    use std::thread;
170
171    #[test]
172    fn disabled_cap_admits_everything_without_tracking() {
173        let limiter = PrincipalConnectionLimiter::new(0);
174        assert!(!limiter.is_enforced());
175        let mut permits = Vec::new();
176        for _ in 0..1_000 {
177            permits.push(limiter.try_acquire("alice").expect("disabled admits"));
178        }
179        // cap==0 short-circuits before the map, so nothing is tracked.
180        assert_eq!(limiter.tracked_principals(), 0);
181        assert_eq!(limiter.rejected_total(), 0);
182    }
183
184    #[test]
185    fn admits_up_to_cap_then_refuses_with_structured_detail() {
186        let limiter = PrincipalConnectionLimiter::new(3);
187        let p1 = limiter.try_acquire("alice").expect("slot 1");
188        let p2 = limiter.try_acquire("alice").expect("slot 2");
189        let p3 = limiter.try_acquire("alice").expect("slot 3");
190        assert_eq!(limiter.current_for("alice"), 3);
191
192        let err = limiter.try_acquire("alice").expect_err("over cap");
193        assert_eq!(
194            err,
195            PrincipalCapExceeded {
196                principal: "alice".to_string(),
197                limit: 3,
198                current: 3,
199            }
200        );
201        assert_eq!(limiter.rejected_total(), 1);
202        drop((p1, p2, p3));
203    }
204
205    #[test]
206    fn dropping_a_permit_frees_a_slot() {
207        let limiter = PrincipalConnectionLimiter::new(1);
208        let p = limiter.try_acquire("bob").expect("first slot");
209        assert!(limiter.try_acquire("bob").is_err());
210        drop(p);
211        // Entry evicted at zero, then re-acquirable.
212        assert_eq!(limiter.current_for("bob"), 0);
213        assert_eq!(limiter.tracked_principals(), 0);
214        let _p = limiter.try_acquire("bob").expect("reacquire after drop");
215        assert_eq!(limiter.current_for("bob"), 1);
216    }
217
218    #[test]
219    fn principals_are_isolated() {
220        let limiter = PrincipalConnectionLimiter::new(1);
221        let _alice = limiter.try_acquire("alice").expect("alice slot");
222        // alice is saturated, but bob has his own independent budget.
223        assert!(limiter.try_acquire("alice").is_err());
224        let _bob = limiter.try_acquire("bob").expect("bob unaffected");
225        assert_eq!(limiter.tracked_principals(), 2);
226    }
227
228    #[test]
229    fn entry_evicted_when_last_permit_drops() {
230        let limiter = PrincipalConnectionLimiter::new(4);
231        let a = limiter.try_acquire("carol").expect("1");
232        let b = limiter.try_acquire("carol").expect("2");
233        assert_eq!(limiter.tracked_principals(), 1);
234        drop(a);
235        assert_eq!(limiter.current_for("carol"), 1);
236        assert_eq!(limiter.tracked_principals(), 1);
237        drop(b);
238        assert_eq!(limiter.tracked_principals(), 0);
239    }
240
241    #[test]
242    fn concurrent_acquire_never_over_issues_per_principal() {
243        // Many threads race the same principal; the high-water count
244        // must never exceed the cap and successes must equal the cap.
245        let cap = 8;
246        let limiter = PrincipalConnectionLimiter::new(cap);
247        let success = Arc::new(AtomicUsize::new(0));
248        let denied = Arc::new(AtomicUsize::new(0));
249        let held: Arc<Mutex<Vec<PrincipalInflightPermit>>> = Arc::new(Mutex::new(Vec::new()));
250
251        let mut handles = Vec::new();
252        for _ in 0..64 {
253            let l = limiter.clone();
254            let s = Arc::clone(&success);
255            let d = Arc::clone(&denied);
256            let h = Arc::clone(&held);
257            handles.push(thread::spawn(move || match l.try_acquire("storm") {
258                Ok(p) => {
259                    s.fetch_add(1, Ordering::Relaxed);
260                    h.lock().unwrap().push(p);
261                }
262                Err(_) => {
263                    d.fetch_add(1, Ordering::Relaxed);
264                }
265            }));
266        }
267        for handle in handles {
268            handle.join().unwrap();
269        }
270        assert_eq!(success.load(Ordering::Relaxed), cap);
271        assert_eq!(denied.load(Ordering::Relaxed), 64 - cap);
272        assert_eq!(limiter.current_for("storm"), cap);
273        assert_eq!(limiter.rejected_total() as usize, 64 - cap);
274
275        held.lock().unwrap().clear();
276        assert_eq!(limiter.current_for("storm"), 0);
277    }
278
279    #[test]
280    fn clone_shares_state() {
281        let a = PrincipalConnectionLimiter::new(2);
282        let b = a.clone();
283        let _p = a.try_acquire("dave").unwrap();
284        assert_eq!(b.current_for("dave"), 1);
285        assert_eq!(b.cap(), 2);
286    }
287}