software_engineering/queueing_theory.rs
1//! # Queueing Theory
2//!
3//! Queueing theory is the mathematical study of waiting lines, and much of
4//! a delivery pipeline actually is a queue: a pull request waiting for a
5//! reviewer, a commit waiting for a CI runner, a ticket waiting to be
6//! picked up. Utilization — how busy a shared, capacity-constrained
7//! resource is, as a proportion of its available capacity — is the key
8//! quantity: wait time does not grow linearly with utilization, it grows
9//! sharply as utilization approaches full capacity.
10//!
11//! ## Formula
12//!
13//! ```text
14//! Utilization = arrival rate / service rate
15//! Queue is stable when utilization < 1.0
16//! ```
17//!
18//! ## Why it matters
19//!
20//! A resource running at 95% busy is often waiting many times longer than
21//! one running at 80%, not just "a little worse" — wait time grows sharply,
22//! not gradually, as utilization approaches capacity. A queue running at
23//! 100% utilization on average has effectively infinite wait time in
24//! practice, because real arrivals are uneven, not perfectly smooth. This
25//! is why "our reviewers are almost always busy" is a warning sign about
26//! wait times to come, not evidence of efficient resourcing, and why
27//! deliberate headroom below full utilization is a design choice, not
28//! waste.
29//!
30//! ## Example
31//!
32//! The topic doc's own worked example: a shared CI fleet found running
33//! above 90% utilization during core hours — well past the point where
34//! queueing theory predicts wait time grows sharply rather than gradually.
35//! A queue with an arrival rate of 9 jobs/hour against a service rate of 10
36//! jobs/hour runs at 90% utilization and is still stable; one with an
37//! arrival rate of 11 jobs/hour against the same service rate is not.
38//!
39//! ```rust
40//! use software_engineering::queueing_theory::{utilization, is_queue_stable};
41//!
42//! let near_capacity = utilization(9.0, 10.0).unwrap();
43//! assert!((near_capacity - 0.9).abs() < 1e-9);
44//! assert!(is_queue_stable(near_capacity));
45//!
46//! let overloaded = utilization(11.0, 10.0).unwrap();
47//! assert!(!is_queue_stable(overloaded));
48//! ```
49//!
50//! ## Pitfalls
51//!
52//! - **Sizing a shared resource's capacity to match its average arrival
53//! rate exactly**: guarantees high utilization and runaway wait times
54//! whenever demand is even briefly uneven. Plan deliberate headroom.
55//! - **Reporting only mean wait time, never a percentile**: hides the long
56//! tail that matters most to the people actually waiting in it.
57//! - **Blending success, failure, and skip into one throughput number**:
58//! a team under pressure can make throughput look healthy by quietly
59//! letting the skip rate (abandoned or silently dropped work) rise. Track
60//! arrival, success, failure, and skip rate as four separate numbers.
61//! - **Treating "our people are always busy" as a compliment**: it is a
62//! symptom of high utilization, the leading cause of long, unpredictable
63//! wait times.
64//!
65//! ## Sources
66//!
67//! - Chapter 2.7, "Queueing theory."
68//! - Little, John D. C. "A Proof for the Queuing Formula: L = λW." *Operations
69//! Research*, 1961.
70//!
71//! Topic doc: software-engineering-metrics/locales/en-001/chapters/02-07-queueing-theory.md
72
73/// Utilization: arrival rate divided by service rate for a shared,
74/// capacity-constrained resource.
75///
76/// # Arguments
77///
78/// * `arrival_rate` — rate at which new work arrives at the resource.
79/// * `service_rate` — rate at which the resource can process work.
80///
81/// # Returns
82///
83/// `Some(utilization)` as a fraction (e.g. `0.9` for 90% busy), or `None`
84/// when `service_rate` is zero.
85///
86/// # Examples
87///
88/// ```rust
89/// use software_engineering::queueing_theory::utilization;
90///
91/// let u = utilization(9.0, 10.0).unwrap();
92/// assert!((u - 0.9).abs() < 1e-9);
93/// assert_eq!(utilization(9.0, 0.0), None);
94/// ```
95#[must_use]
96pub fn utilization(arrival_rate: f64, service_rate: f64) -> Option<f64> {
97 if service_rate == 0.0 {
98 None
99 } else {
100 Some(arrival_rate / service_rate)
101 }
102}
103
104/// Whether a queue is stable: utilization strictly less than 1.0.
105///
106/// A queue at or above 100% utilization has, in practice, unboundedly
107/// growing wait time, because real arrivals are uneven rather than
108/// perfectly smooth. Wait time grows sharply, not gradually, as utilization
109/// approaches this threshold — treat utilization consistently near 1.0 as
110/// an early warning, not just outright instability as the only concern.
111///
112/// # Arguments
113///
114/// * `utilization` — the resource's utilization as a fraction (see
115/// [`utilization`]).
116///
117/// # Returns
118///
119/// `true` if `utilization < 1.0` (stable), `false` otherwise.
120///
121/// # Examples
122///
123/// ```rust
124/// use software_engineering::queueing_theory::is_queue_stable;
125///
126/// assert!(is_queue_stable(0.9));
127/// assert!(!is_queue_stable(1.0));
128/// assert!(!is_queue_stable(1.1));
129/// ```
130#[must_use]
131pub fn is_queue_stable(utilization: f64) -> bool {
132 utilization < 1.0
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 // "A utilization analysis found the fleet running above 90% busy during
140 // core hours, well past the point where queueing theory predicts wait
141 // time grows sharply."
142 #[test]
143 fn utilization_near_ninety_percent_is_still_stable_but_near_capacity() {
144 let u = utilization(9.0, 10.0).unwrap();
145 assert!((u - 0.9).abs() < 1e-9);
146 assert!(is_queue_stable(u));
147 assert_eq!(utilization(9.0, 0.0), None);
148 }
149
150 // "A queue running at 100% utilization on average has effectively
151 // infinite wait time in practice."
152 #[test]
153 fn a_queue_at_or_above_full_utilization_is_not_stable() {
154 assert!(!is_queue_stable(1.0));
155 let overloaded = utilization(11.0, 10.0).unwrap();
156 assert!(!is_queue_stable(overloaded));
157 }
158}