Skip to main content

relay_knowledge/net/
qos.rs

1//! QoS admission policy for inbound and outbound network work.
2//!
3//! The policy keeps HTTP clients, HTTP servers, and background network tasks
4//! inside explicit resource budgets before they can allocate unbounded work.
5
6use std::{
7    error::Error,
8    fmt,
9    sync::{Arc, Mutex},
10};
11
12use crate::env::NetworkEnvOverrides;
13
14pub const DEFAULT_MAX_CONNECTIONS: usize = 1024;
15pub const DEFAULT_MAX_IN_FLIGHT_REQUESTS: usize = 256;
16pub const DEFAULT_MAX_QUEUE_DEPTH: usize = 512;
17
18/// Bounded resource policy for network admission decisions.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct QosPolicy {
21    pub max_connections: usize,
22    pub max_in_flight_requests: usize,
23    pub max_queue_depth: usize,
24}
25
26impl QosPolicy {
27    /// Creates a policy and rejects zero-sized budgets.
28    pub fn new(
29        max_connections: usize,
30        max_in_flight_requests: usize,
31        max_queue_depth: usize,
32    ) -> Result<Self, QosPolicyError> {
33        ensure_positive("max_connections", max_connections)?;
34        ensure_positive("max_in_flight_requests", max_in_flight_requests)?;
35        ensure_positive("max_queue_depth", max_queue_depth)?;
36
37        Ok(Self {
38            max_connections,
39            max_in_flight_requests,
40            max_queue_depth,
41        })
42    }
43
44    /// Applies environment overrides to the default interactive QoS budgets.
45    pub fn from_overrides(overrides: &NetworkEnvOverrides) -> Result<Self, QosPolicyError> {
46        Self::new(
47            overrides
48                .qos_max_connections
49                .unwrap_or(DEFAULT_MAX_CONNECTIONS),
50            overrides
51                .qos_max_in_flight_requests
52                .unwrap_or(DEFAULT_MAX_IN_FLIGHT_REQUESTS),
53            overrides
54                .qos_max_queue_depth
55                .unwrap_or(DEFAULT_MAX_QUEUE_DEPTH),
56        )
57    }
58
59    /// Evaluates current resource usage before admitting network work.
60    pub fn evaluate(&self, snapshot: QosSnapshot) -> AdmissionDecision {
61        if snapshot.connections >= self.max_connections {
62            return AdmissionDecision::Reject(RejectReason::ConnectionBudgetExceeded);
63        }
64
65        if snapshot.in_flight_requests >= self.max_in_flight_requests {
66            return AdmissionDecision::Reject(RejectReason::RequestBudgetExceeded);
67        }
68
69        if snapshot.queued_requests >= self.max_queue_depth {
70            return AdmissionDecision::Reject(RejectReason::QueueBudgetExceeded);
71        }
72
73        AdmissionDecision::Admit
74    }
75}
76
77/// Point-in-time network resource usage for QoS decisions.
78#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
79pub struct QosSnapshot {
80    pub connections: usize,
81    pub in_flight_requests: usize,
82    pub queued_requests: usize,
83}
84
85/// Result of a QoS admission check.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum AdmissionDecision {
88    Admit,
89    Reject(RejectReason),
90}
91
92/// Reason attached to a rejected network operation.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum RejectReason {
95    ConnectionBudgetExceeded,
96    RequestBudgetExceeded,
97    QueueBudgetExceeded,
98}
99
100/// Runtime QoS counters used by inbound protocol adapters.
101#[derive(Debug, Clone, Default)]
102pub struct QosRuntime {
103    usage: Arc<Mutex<QosSnapshot>>,
104}
105
106impl QosRuntime {
107    /// Reserves space in the bounded request queue before active admission.
108    pub fn reserve_queue(&self, policy: &QosPolicy) -> Result<QosPermit, RejectReason> {
109        let mut usage = self
110            .usage
111            .lock()
112            .unwrap_or_else(|poisoned| poisoned.into_inner());
113        if usage.queued_requests >= policy.max_queue_depth {
114            return Err(RejectReason::QueueBudgetExceeded);
115        }
116
117        usage.queued_requests += 1;
118        Ok(QosPermit {
119            runtime: self.clone(),
120            kind: QosPermitKind::Queue,
121            released: false,
122        })
123    }
124
125    /// Applies queue and active request budgets atomically for immediate admission.
126    pub fn admit_queued_request(&self, policy: &QosPolicy) -> Result<QosPermit, RejectReason> {
127        let mut usage = self
128            .usage
129            .lock()
130            .unwrap_or_else(|poisoned| poisoned.into_inner());
131        if usage.queued_requests >= policy.max_queue_depth {
132            return Err(RejectReason::QueueBudgetExceeded);
133        }
134
135        if usage.in_flight_requests >= policy.max_in_flight_requests {
136            return Err(RejectReason::RequestBudgetExceeded);
137        }
138
139        usage.in_flight_requests += 1;
140        Ok(QosPermit {
141            runtime: self.clone(),
142            kind: QosPermitKind::Request,
143            released: false,
144        })
145    }
146
147    /// Attempts to admit one inbound request and returns a release-on-drop permit.
148    pub fn admit_request(&self, policy: &QosPolicy) -> Result<QosPermit, RejectReason> {
149        let mut usage = self
150            .usage
151            .lock()
152            .unwrap_or_else(|poisoned| poisoned.into_inner());
153        if usage.in_flight_requests >= policy.max_in_flight_requests {
154            return Err(RejectReason::RequestBudgetExceeded);
155        }
156
157        usage.in_flight_requests += 1;
158        Ok(QosPermit {
159            runtime: self.clone(),
160            kind: QosPermitKind::Request,
161            released: false,
162        })
163    }
164
165    /// Attempts to admit one open network connection.
166    pub fn admit_connection(&self, policy: &QosPolicy) -> Result<QosPermit, RejectReason> {
167        let mut usage = self
168            .usage
169            .lock()
170            .unwrap_or_else(|poisoned| poisoned.into_inner());
171        if usage.connections >= policy.max_connections {
172            return Err(RejectReason::ConnectionBudgetExceeded);
173        }
174
175        usage.connections += 1;
176        Ok(QosPermit {
177            runtime: self.clone(),
178            kind: QosPermitKind::Connection,
179            released: false,
180        })
181    }
182
183    /// Returns the current request budget snapshot for diagnostics and tests.
184    pub fn snapshot(&self) -> QosSnapshot {
185        *self
186            .usage
187            .lock()
188            .unwrap_or_else(|poisoned| poisoned.into_inner())
189    }
190
191    fn release(&self, kind: QosPermitKind) {
192        let mut usage = self
193            .usage
194            .lock()
195            .unwrap_or_else(|poisoned| poisoned.into_inner());
196        match kind {
197            QosPermitKind::Connection => {
198                usage.connections = usage.connections.saturating_sub(1);
199            }
200            QosPermitKind::Request => {
201                usage.in_flight_requests = usage.in_flight_requests.saturating_sub(1);
202            }
203            QosPermitKind::Queue => {
204                usage.queued_requests = usage.queued_requests.saturating_sub(1);
205            }
206        }
207    }
208}
209
210#[derive(Debug, Clone, Copy)]
211enum QosPermitKind {
212    Connection,
213    Request,
214    Queue,
215}
216
217/// Admission permit that releases its QoS budget on drop.
218#[derive(Debug)]
219pub struct QosPermit {
220    runtime: QosRuntime,
221    kind: QosPermitKind,
222    released: bool,
223}
224
225impl Drop for QosPermit {
226    fn drop(&mut self) {
227        if !self.released {
228            self.runtime.release(self.kind);
229            self.released = true;
230        }
231    }
232}
233
234/// QoS policy validation error.
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct QosPolicyError {
237    pub field: &'static str,
238}
239
240impl fmt::Display for QosPolicyError {
241    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242        write!(formatter, "{} must be greater than zero", self.field)
243    }
244}
245
246impl Error for QosPolicyError {}
247
248fn ensure_positive(field: &'static str, value: usize) -> Result<(), QosPolicyError> {
249    if value == 0 {
250        return Err(QosPolicyError { field });
251    }
252
253    Ok(())
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn admits_work_inside_all_budgets() {
262        let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
263
264        let decision = policy.evaluate(QosSnapshot {
265            connections: 1,
266            in_flight_requests: 1,
267            queued_requests: 1,
268        });
269
270        assert_eq!(decision, AdmissionDecision::Admit);
271    }
272
273    #[test]
274    fn rejects_when_connection_budget_is_exhausted() {
275        let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
276
277        let decision = policy.evaluate(QosSnapshot {
278            connections: 2,
279            in_flight_requests: 1,
280            queued_requests: 1,
281        });
282
283        assert_eq!(
284            decision,
285            AdmissionDecision::Reject(RejectReason::ConnectionBudgetExceeded)
286        );
287    }
288
289    #[test]
290    fn rejects_when_request_budget_is_exhausted() {
291        let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
292
293        let decision = policy.evaluate(QosSnapshot {
294            connections: 1,
295            in_flight_requests: 2,
296            queued_requests: 1,
297        });
298
299        assert_eq!(
300            decision,
301            AdmissionDecision::Reject(RejectReason::RequestBudgetExceeded)
302        );
303    }
304
305    #[test]
306    fn rejects_when_queue_budget_is_exhausted() {
307        let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
308
309        let decision = policy.evaluate(QosSnapshot {
310            connections: 1,
311            in_flight_requests: 1,
312            queued_requests: 2,
313        });
314
315        assert_eq!(
316            decision,
317            AdmissionDecision::Reject(RejectReason::QueueBudgetExceeded)
318        );
319    }
320
321    #[test]
322    fn rejects_zero_sized_budgets() {
323        let error = QosPolicy::new(0, 1, 1).expect_err("zero budget should fail");
324
325        assert_eq!(
326            error,
327            QosPolicyError {
328                field: "max_connections"
329            }
330        );
331    }
332
333    #[test]
334    fn permit_releases_in_flight_budget_on_drop() {
335        let runtime = QosRuntime::default();
336        let policy = QosPolicy::new(2, 1, 1).expect("policy should build");
337        let permit = runtime
338            .admit_request(&policy)
339            .expect("first request should enter");
340
341        assert_eq!(runtime.snapshot().in_flight_requests, 1);
342        assert_eq!(
343            runtime
344                .admit_request(&policy)
345                .expect_err("second request should fail"),
346            RejectReason::RequestBudgetExceeded
347        );
348
349        drop(permit);
350
351        assert_eq!(runtime.snapshot().in_flight_requests, 0);
352        assert!(runtime.admit_request(&policy).is_ok());
353    }
354
355    #[test]
356    fn connection_and_request_permits_update_independent_budgets() {
357        let runtime = QosRuntime::default();
358        let policy = QosPolicy::new(1, 1, 1).expect("policy should build");
359        let connection = runtime
360            .admit_connection(&policy)
361            .expect("connection should enter");
362
363        assert_eq!(runtime.snapshot().connections, 1);
364        assert_eq!(runtime.snapshot().in_flight_requests, 0);
365        assert_eq!(
366            runtime
367                .admit_connection(&policy)
368                .expect_err("second connection should fail"),
369            RejectReason::ConnectionBudgetExceeded
370        );
371
372        let request = runtime
373            .admit_request(&policy)
374            .expect("request on existing connection should enter");
375
376        assert_eq!(runtime.snapshot().connections, 1);
377        assert_eq!(runtime.snapshot().in_flight_requests, 1);
378
379        drop(request);
380        assert_eq!(runtime.snapshot().connections, 1);
381        assert_eq!(runtime.snapshot().in_flight_requests, 0);
382
383        drop(connection);
384        assert_eq!(runtime.snapshot().connections, 0);
385    }
386
387    #[test]
388    fn queue_permit_tracks_waiting_budget_independently() {
389        let runtime = QosRuntime::default();
390        let policy = QosPolicy::new(1, 1, 1).expect("policy should build");
391        let queued = runtime
392            .reserve_queue(&policy)
393            .expect("first queued request should enter");
394
395        assert_eq!(runtime.snapshot().queued_requests, 1);
396        assert_eq!(
397            runtime
398                .reserve_queue(&policy)
399                .expect_err("second queued request should fail"),
400            RejectReason::QueueBudgetExceeded
401        );
402
403        drop(queued);
404        assert_eq!(runtime.snapshot().queued_requests, 0);
405    }
406
407    #[test]
408    fn queued_request_admission_preserves_budget_invariants() {
409        let runtime = QosRuntime::default();
410        let policy = QosPolicy::new(1, 1, 1).expect("policy should build");
411        let queued = runtime
412            .reserve_queue(&policy)
413            .expect("queued request should reserve budget");
414
415        assert_eq!(
416            runtime
417                .admit_queued_request(&policy)
418                .expect_err("full queue should reject admission"),
419            RejectReason::QueueBudgetExceeded
420        );
421        drop(queued);
422
423        let active = runtime
424            .admit_queued_request(&policy)
425            .expect("request should enter through queued admission");
426
427        assert_eq!(runtime.snapshot().queued_requests, 0);
428        assert_eq!(runtime.snapshot().in_flight_requests, 1);
429        assert_eq!(
430            runtime
431                .admit_queued_request(&policy)
432                .expect_err("active request budget should reject admission"),
433            RejectReason::RequestBudgetExceeded
434        );
435        assert_eq!(runtime.snapshot().queued_requests, 0);
436
437        drop(active);
438        assert_eq!(runtime.snapshot().in_flight_requests, 0);
439    }
440}