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_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 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 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 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#[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#[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}