1use 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#[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 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 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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum AdmissionDecision {
88 Admit,
89 Reject(RejectReason),
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum RejectReason {
95 ConnectionBudgetExceeded,
96 RequestBudgetExceeded,
97 QueueBudgetExceeded,
98}
99
100#[derive(Debug, Clone, Default)]
102pub struct QosRuntime {
103 usage: Arc<Mutex<QosSnapshot>>,
104}
105
106impl QosRuntime {
107 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 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 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 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#[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#[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}