Skip to main content

relay_knowledge/net/
qos.rs

1//! QoS admission policy for inbound and outbound network work.
2//!
3//! The policy keeps future HTTP, indexing, and background network tasks inside
4//! 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    /// Attempts to admit one inbound request and returns a release-on-drop permit.
126    pub fn admit_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.in_flight_requests >= policy.max_in_flight_requests {
132            return Err(RejectReason::RequestBudgetExceeded);
133        }
134
135        usage.in_flight_requests += 1;
136        Ok(QosPermit {
137            runtime: self.clone(),
138            kind: QosPermitKind::Request,
139            released: false,
140        })
141    }
142
143    /// Attempts to admit one open network connection.
144    pub fn admit_connection(&self, policy: &QosPolicy) -> Result<QosPermit, RejectReason> {
145        let mut usage = self
146            .usage
147            .lock()
148            .unwrap_or_else(|poisoned| poisoned.into_inner());
149        if usage.connections >= policy.max_connections {
150            return Err(RejectReason::ConnectionBudgetExceeded);
151        }
152
153        usage.connections += 1;
154        Ok(QosPermit {
155            runtime: self.clone(),
156            kind: QosPermitKind::Connection,
157            released: false,
158        })
159    }
160
161    /// Returns the current request budget snapshot for diagnostics and tests.
162    pub fn snapshot(&self) -> QosSnapshot {
163        *self
164            .usage
165            .lock()
166            .unwrap_or_else(|poisoned| poisoned.into_inner())
167    }
168
169    fn release(&self, kind: QosPermitKind) {
170        let mut usage = self
171            .usage
172            .lock()
173            .unwrap_or_else(|poisoned| poisoned.into_inner());
174        match kind {
175            QosPermitKind::Connection => {
176                usage.connections = usage.connections.saturating_sub(1);
177            }
178            QosPermitKind::Request => {
179                usage.in_flight_requests = usage.in_flight_requests.saturating_sub(1);
180            }
181            QosPermitKind::Queue => {
182                usage.queued_requests = usage.queued_requests.saturating_sub(1);
183            }
184        }
185    }
186}
187
188#[derive(Debug, Clone, Copy)]
189enum QosPermitKind {
190    Connection,
191    Request,
192    Queue,
193}
194
195/// Admission permit that releases its QoS budget on drop.
196#[derive(Debug)]
197pub struct QosPermit {
198    runtime: QosRuntime,
199    kind: QosPermitKind,
200    released: bool,
201}
202
203impl Drop for QosPermit {
204    fn drop(&mut self) {
205        if !self.released {
206            self.runtime.release(self.kind);
207            self.released = true;
208        }
209    }
210}
211
212/// QoS policy validation error.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct QosPolicyError {
215    pub field: &'static str,
216}
217
218impl fmt::Display for QosPolicyError {
219    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
220        write!(formatter, "{} must be greater than zero", self.field)
221    }
222}
223
224impl Error for QosPolicyError {}
225
226fn ensure_positive(field: &'static str, value: usize) -> Result<(), QosPolicyError> {
227    if value == 0 {
228        return Err(QosPolicyError { field });
229    }
230
231    Ok(())
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn admits_work_inside_all_budgets() {
240        let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
241
242        let decision = policy.evaluate(QosSnapshot {
243            connections: 1,
244            in_flight_requests: 1,
245            queued_requests: 1,
246        });
247
248        assert_eq!(decision, AdmissionDecision::Admit);
249    }
250
251    #[test]
252    fn rejects_when_connection_budget_is_exhausted() {
253        let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
254
255        let decision = policy.evaluate(QosSnapshot {
256            connections: 2,
257            in_flight_requests: 1,
258            queued_requests: 1,
259        });
260
261        assert_eq!(
262            decision,
263            AdmissionDecision::Reject(RejectReason::ConnectionBudgetExceeded)
264        );
265    }
266
267    #[test]
268    fn rejects_when_request_budget_is_exhausted() {
269        let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
270
271        let decision = policy.evaluate(QosSnapshot {
272            connections: 1,
273            in_flight_requests: 2,
274            queued_requests: 1,
275        });
276
277        assert_eq!(
278            decision,
279            AdmissionDecision::Reject(RejectReason::RequestBudgetExceeded)
280        );
281    }
282
283    #[test]
284    fn rejects_when_queue_budget_is_exhausted() {
285        let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
286
287        let decision = policy.evaluate(QosSnapshot {
288            connections: 1,
289            in_flight_requests: 1,
290            queued_requests: 2,
291        });
292
293        assert_eq!(
294            decision,
295            AdmissionDecision::Reject(RejectReason::QueueBudgetExceeded)
296        );
297    }
298
299    #[test]
300    fn rejects_zero_sized_budgets() {
301        let error = QosPolicy::new(0, 1, 1).expect_err("zero budget should fail");
302
303        assert_eq!(
304            error,
305            QosPolicyError {
306                field: "max_connections"
307            }
308        );
309    }
310
311    #[test]
312    fn permit_releases_in_flight_budget_on_drop() {
313        let runtime = QosRuntime::default();
314        let policy = QosPolicy::new(2, 1, 1).expect("policy should build");
315        let permit = runtime
316            .admit_request(&policy)
317            .expect("first request should enter");
318
319        assert_eq!(runtime.snapshot().in_flight_requests, 1);
320        assert_eq!(
321            runtime
322                .admit_request(&policy)
323                .expect_err("second request should fail"),
324            RejectReason::RequestBudgetExceeded
325        );
326
327        drop(permit);
328
329        assert_eq!(runtime.snapshot().in_flight_requests, 0);
330        assert!(runtime.admit_request(&policy).is_ok());
331    }
332
333    #[test]
334    fn connection_and_request_permits_update_independent_budgets() {
335        let runtime = QosRuntime::default();
336        let policy = QosPolicy::new(1, 1, 1).expect("policy should build");
337        let connection = runtime
338            .admit_connection(&policy)
339            .expect("connection should enter");
340
341        assert_eq!(runtime.snapshot().connections, 1);
342        assert_eq!(runtime.snapshot().in_flight_requests, 0);
343        assert_eq!(
344            runtime
345                .admit_connection(&policy)
346                .expect_err("second connection should fail"),
347            RejectReason::ConnectionBudgetExceeded
348        );
349
350        let request = runtime
351            .admit_request(&policy)
352            .expect("request on existing connection should enter");
353
354        assert_eq!(runtime.snapshot().connections, 1);
355        assert_eq!(runtime.snapshot().in_flight_requests, 1);
356
357        drop(request);
358        assert_eq!(runtime.snapshot().connections, 1);
359        assert_eq!(runtime.snapshot().in_flight_requests, 0);
360
361        drop(connection);
362        assert_eq!(runtime.snapshot().connections, 0);
363    }
364
365    #[test]
366    fn queue_permit_tracks_waiting_budget_independently() {
367        let runtime = QosRuntime::default();
368        let policy = QosPolicy::new(1, 1, 1).expect("policy should build");
369        let queued = runtime
370            .reserve_queue(&policy)
371            .expect("first queued request should enter");
372
373        assert_eq!(runtime.snapshot().queued_requests, 1);
374        assert_eq!(
375            runtime
376                .reserve_queue(&policy)
377                .expect_err("second queued request should fail"),
378            RejectReason::QueueBudgetExceeded
379        );
380
381        drop(queued);
382        assert_eq!(runtime.snapshot().queued_requests, 0);
383    }
384}