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, Default, PartialEq, Eq)]
87pub struct QosDiagnosticsSnapshot {
88 pub usage: QosSnapshot,
89 pub admitted_total: u64,
90 pub queued_total: u64,
91 pub rejected_total: u64,
92 pub timed_out_total: u64,
93 pub cancelled_total: u64,
94 pub dropped_total: u64,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum AdmissionDecision {
100 Admit,
101 Reject(RejectReason),
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum RejectReason {
107 ConnectionBudgetExceeded,
108 RequestBudgetExceeded,
109 QueueBudgetExceeded,
110}
111
112impl RejectReason {
113 pub const fn as_str(self) -> &'static str {
115 match self {
116 Self::ConnectionBudgetExceeded => "connection_budget_exceeded",
117 Self::RequestBudgetExceeded => "request_budget_exceeded",
118 Self::QueueBudgetExceeded => "queue_budget_exceeded",
119 }
120 }
121}
122
123#[derive(Debug, Clone, Default)]
125pub struct QosRuntime {
126 state: Arc<Mutex<QosState>>,
127}
128
129#[derive(Debug, Clone, Copy, Default)]
130struct QosState {
131 usage: QosSnapshot,
132 counters: QosCounters,
133}
134
135#[derive(Debug, Clone, Copy, Default)]
136struct QosCounters {
137 admitted_total: u64,
138 queued_total: u64,
139 rejected_total: u64,
140 timed_out_total: u64,
141 cancelled_total: u64,
142 dropped_total: u64,
143}
144
145impl QosRuntime {
146 pub fn reserve_queue(&self, policy: &QosPolicy) -> Result<QosPermit, RejectReason> {
148 let mut state = self
149 .state
150 .lock()
151 .unwrap_or_else(|poisoned| poisoned.into_inner());
152 if state.usage.queued_requests >= policy.max_queue_depth {
153 state.record_rejection();
154 return Err(RejectReason::QueueBudgetExceeded);
155 }
156
157 state.usage.queued_requests += 1;
158 state.counters.queued_total = state.counters.queued_total.saturating_add(1);
159 Ok(QosPermit {
160 runtime: self.clone(),
161 kind: QosPermitKind::Queue,
162 released: false,
163 })
164 }
165
166 pub fn admit_queued_request(&self, policy: &QosPolicy) -> Result<QosPermit, RejectReason> {
168 let mut state = self
169 .state
170 .lock()
171 .unwrap_or_else(|poisoned| poisoned.into_inner());
172 if state.usage.queued_requests >= policy.max_queue_depth {
173 state.record_rejection();
174 return Err(RejectReason::QueueBudgetExceeded);
175 }
176
177 if state.usage.in_flight_requests >= policy.max_in_flight_requests {
178 state.record_rejection();
179 return Err(RejectReason::RequestBudgetExceeded);
180 }
181
182 state.usage.in_flight_requests += 1;
183 state.counters.queued_total = state.counters.queued_total.saturating_add(1);
184 state.counters.admitted_total = state.counters.admitted_total.saturating_add(1);
185 Ok(QosPermit {
186 runtime: self.clone(),
187 kind: QosPermitKind::Request,
188 released: false,
189 })
190 }
191
192 pub fn admit_request(&self, policy: &QosPolicy) -> Result<QosPermit, RejectReason> {
194 let mut state = self
195 .state
196 .lock()
197 .unwrap_or_else(|poisoned| poisoned.into_inner());
198 if state.usage.in_flight_requests >= policy.max_in_flight_requests {
199 state.record_rejection();
200 return Err(RejectReason::RequestBudgetExceeded);
201 }
202
203 state.usage.in_flight_requests += 1;
204 state.counters.admitted_total = state.counters.admitted_total.saturating_add(1);
205 Ok(QosPermit {
206 runtime: self.clone(),
207 kind: QosPermitKind::Request,
208 released: false,
209 })
210 }
211
212 pub fn admit_connection(&self, policy: &QosPolicy) -> Result<QosPermit, RejectReason> {
214 let mut state = self
215 .state
216 .lock()
217 .unwrap_or_else(|poisoned| poisoned.into_inner());
218 if state.usage.connections >= policy.max_connections {
219 state.record_rejection();
220 return Err(RejectReason::ConnectionBudgetExceeded);
221 }
222
223 state.usage.connections += 1;
224 state.counters.admitted_total = state.counters.admitted_total.saturating_add(1);
225 Ok(QosPermit {
226 runtime: self.clone(),
227 kind: QosPermitKind::Connection,
228 released: false,
229 })
230 }
231
232 pub fn snapshot(&self) -> QosSnapshot {
234 self.state
235 .lock()
236 .unwrap_or_else(|poisoned| poisoned.into_inner())
237 .usage
238 }
239
240 pub fn diagnostics_snapshot(&self) -> QosDiagnosticsSnapshot {
242 let state = self
243 .state
244 .lock()
245 .unwrap_or_else(|poisoned| poisoned.into_inner());
246 QosDiagnosticsSnapshot {
247 usage: state.usage,
248 admitted_total: state.counters.admitted_total,
249 queued_total: state.counters.queued_total,
250 rejected_total: state.counters.rejected_total,
251 timed_out_total: state.counters.timed_out_total,
252 cancelled_total: state.counters.cancelled_total,
253 dropped_total: state.counters.dropped_total,
254 }
255 }
256
257 pub fn record_timed_out(&self) {
259 let mut state = self
260 .state
261 .lock()
262 .unwrap_or_else(|poisoned| poisoned.into_inner());
263 state.counters.timed_out_total = state.counters.timed_out_total.saturating_add(1);
264 }
265
266 pub fn record_cancelled(&self) {
268 let mut state = self
269 .state
270 .lock()
271 .unwrap_or_else(|poisoned| poisoned.into_inner());
272 state.counters.cancelled_total = state.counters.cancelled_total.saturating_add(1);
273 }
274
275 pub fn record_dropped(&self) {
277 let mut state = self
278 .state
279 .lock()
280 .unwrap_or_else(|poisoned| poisoned.into_inner());
281 state.counters.dropped_total = state.counters.dropped_total.saturating_add(1);
282 }
283
284 fn release(&self, kind: QosPermitKind) {
285 if matches!(kind, QosPermitKind::Noop) {
286 return;
287 }
288 let mut state = self
289 .state
290 .lock()
291 .unwrap_or_else(|poisoned| poisoned.into_inner());
292 match kind {
293 QosPermitKind::Connection => {
294 state.usage.connections = state.usage.connections.saturating_sub(1);
295 }
296 QosPermitKind::Request => {
297 state.usage.in_flight_requests = state.usage.in_flight_requests.saturating_sub(1);
298 }
299 QosPermitKind::Queue => {
300 state.usage.queued_requests = state.usage.queued_requests.saturating_sub(1);
301 }
302 QosPermitKind::Noop => {}
303 }
304 }
305}
306
307impl QosState {
308 fn record_rejection(&mut self) {
309 self.counters.rejected_total = self.counters.rejected_total.saturating_add(1);
310 }
311}
312
313#[derive(Debug, Clone, Copy)]
314enum QosPermitKind {
315 Connection,
316 Request,
317 Queue,
318 Noop,
319}
320
321#[derive(Debug)]
323pub struct QosPermit {
324 runtime: QosRuntime,
325 kind: QosPermitKind,
326 released: bool,
327}
328
329impl QosPermit {
330 pub(crate) fn already_admitted(runtime: QosRuntime) -> Self {
332 Self {
333 runtime,
334 kind: QosPermitKind::Noop,
335 released: false,
336 }
337 }
338}
339
340impl Drop for QosPermit {
341 fn drop(&mut self) {
342 if !self.released {
343 self.runtime.release(self.kind);
344 self.released = true;
345 }
346 }
347}
348
349#[derive(Debug, Clone, PartialEq, Eq)]
351pub struct QosPolicyError {
352 pub field: &'static str,
353}
354
355impl fmt::Display for QosPolicyError {
356 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
357 write!(formatter, "{} must be greater than zero", self.field)
358 }
359}
360
361impl Error for QosPolicyError {}
362
363fn ensure_positive(field: &'static str, value: usize) -> Result<(), QosPolicyError> {
364 if value == 0 {
365 return Err(QosPolicyError { field });
366 }
367
368 Ok(())
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn admits_work_inside_all_budgets() {
377 let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
378
379 let decision = policy.evaluate(QosSnapshot {
380 connections: 1,
381 in_flight_requests: 1,
382 queued_requests: 1,
383 });
384
385 assert_eq!(decision, AdmissionDecision::Admit);
386 }
387
388 #[test]
389 fn rejects_when_connection_budget_is_exhausted() {
390 let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
391
392 let decision = policy.evaluate(QosSnapshot {
393 connections: 2,
394 in_flight_requests: 1,
395 queued_requests: 1,
396 });
397
398 assert_eq!(
399 decision,
400 AdmissionDecision::Reject(RejectReason::ConnectionBudgetExceeded)
401 );
402 }
403
404 #[test]
405 fn rejects_when_request_budget_is_exhausted() {
406 let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
407
408 let decision = policy.evaluate(QosSnapshot {
409 connections: 1,
410 in_flight_requests: 2,
411 queued_requests: 1,
412 });
413
414 assert_eq!(
415 decision,
416 AdmissionDecision::Reject(RejectReason::RequestBudgetExceeded)
417 );
418 }
419
420 #[test]
421 fn rejects_when_queue_budget_is_exhausted() {
422 let policy = QosPolicy::new(2, 2, 2).expect("policy should build");
423
424 let decision = policy.evaluate(QosSnapshot {
425 connections: 1,
426 in_flight_requests: 1,
427 queued_requests: 2,
428 });
429
430 assert_eq!(
431 decision,
432 AdmissionDecision::Reject(RejectReason::QueueBudgetExceeded)
433 );
434 }
435
436 #[test]
437 fn rejects_zero_sized_budgets() {
438 let error = QosPolicy::new(0, 1, 1).expect_err("zero budget should fail");
439
440 assert_eq!(
441 error,
442 QosPolicyError {
443 field: "max_connections"
444 }
445 );
446 }
447
448 #[test]
449 fn permit_releases_in_flight_budget_on_drop() {
450 let runtime = QosRuntime::default();
451 let policy = QosPolicy::new(2, 1, 1).expect("policy should build");
452 let permit = runtime
453 .admit_request(&policy)
454 .expect("first request should enter");
455
456 assert_eq!(runtime.snapshot().in_flight_requests, 1);
457 assert_eq!(
458 runtime
459 .admit_request(&policy)
460 .expect_err("second request should fail"),
461 RejectReason::RequestBudgetExceeded
462 );
463
464 drop(permit);
465
466 assert_eq!(runtime.snapshot().in_flight_requests, 0);
467 assert!(runtime.admit_request(&policy).is_ok());
468 }
469
470 #[test]
471 fn connection_and_request_permits_update_independent_budgets() {
472 let runtime = QosRuntime::default();
473 let policy = QosPolicy::new(1, 1, 1).expect("policy should build");
474 let connection = runtime
475 .admit_connection(&policy)
476 .expect("connection should enter");
477
478 assert_eq!(runtime.snapshot().connections, 1);
479 assert_eq!(runtime.snapshot().in_flight_requests, 0);
480 assert_eq!(
481 runtime
482 .admit_connection(&policy)
483 .expect_err("second connection should fail"),
484 RejectReason::ConnectionBudgetExceeded
485 );
486
487 let request = runtime
488 .admit_request(&policy)
489 .expect("request on existing connection should enter");
490
491 assert_eq!(runtime.snapshot().connections, 1);
492 assert_eq!(runtime.snapshot().in_flight_requests, 1);
493
494 drop(request);
495 assert_eq!(runtime.snapshot().connections, 1);
496 assert_eq!(runtime.snapshot().in_flight_requests, 0);
497
498 drop(connection);
499 assert_eq!(runtime.snapshot().connections, 0);
500 }
501
502 #[test]
503 fn queue_permit_tracks_waiting_budget_independently() {
504 let runtime = QosRuntime::default();
505 let policy = QosPolicy::new(1, 1, 1).expect("policy should build");
506 let queued = runtime
507 .reserve_queue(&policy)
508 .expect("first queued request should enter");
509
510 assert_eq!(runtime.snapshot().queued_requests, 1);
511 assert_eq!(
512 runtime
513 .reserve_queue(&policy)
514 .expect_err("second queued request should fail"),
515 RejectReason::QueueBudgetExceeded
516 );
517
518 drop(queued);
519 assert_eq!(runtime.snapshot().queued_requests, 0);
520 }
521
522 #[test]
523 fn queued_request_admission_preserves_budget_invariants() {
524 let runtime = QosRuntime::default();
525 let policy = QosPolicy::new(1, 1, 1).expect("policy should build");
526 let queued = runtime
527 .reserve_queue(&policy)
528 .expect("queued request should reserve budget");
529
530 assert_eq!(
531 runtime
532 .admit_queued_request(&policy)
533 .expect_err("full queue should reject admission"),
534 RejectReason::QueueBudgetExceeded
535 );
536 drop(queued);
537
538 let active = runtime
539 .admit_queued_request(&policy)
540 .expect("request should enter through queued admission");
541
542 assert_eq!(runtime.snapshot().queued_requests, 0);
543 assert_eq!(runtime.snapshot().in_flight_requests, 1);
544 assert_eq!(
545 runtime
546 .admit_queued_request(&policy)
547 .expect_err("active request budget should reject admission"),
548 RejectReason::RequestBudgetExceeded
549 );
550 assert_eq!(runtime.snapshot().queued_requests, 0);
551
552 drop(active);
553 assert_eq!(runtime.snapshot().in_flight_requests, 0);
554 }
555
556 #[test]
557 fn diagnostics_snapshot_records_admission_and_overload_outcomes() {
558 let runtime = QosRuntime::default();
559 let policy = QosPolicy::new(1, 1, 1).expect("policy should build");
560 let permit = runtime
561 .admit_request(&policy)
562 .expect("first request should enter");
563
564 assert_eq!(
565 runtime
566 .admit_request(&policy)
567 .expect_err("second request should exceed active budget"),
568 RejectReason::RequestBudgetExceeded
569 );
570 runtime.record_timed_out();
571 runtime.record_cancelled();
572 runtime.record_dropped();
573
574 let diagnostics = runtime.diagnostics_snapshot();
575 assert_eq!(diagnostics.usage.in_flight_requests, 1);
576 assert_eq!(diagnostics.admitted_total, 1);
577 assert_eq!(diagnostics.rejected_total, 1);
578 assert_eq!(diagnostics.timed_out_total, 1);
579 assert_eq!(diagnostics.cancelled_total, 1);
580 assert_eq!(diagnostics.dropped_total, 1);
581
582 drop(permit);
583 assert_eq!(runtime.diagnostics_snapshot().usage.in_flight_requests, 0);
584 }
585}