1use crate::error::PoolsimError;
15
16pub fn utilisation(lambda: f64, mu: f64, c: u32) -> f64 {
18 if c == 0 || mu <= 0.0 {
19 return f64::INFINITY;
20 }
21 lambda / (c as f64 * mu)
22}
23
24pub fn erlang_c(lambda: f64, mu: f64, c: u32) -> Result<f64, PoolsimError> {
31 if c == 0 {
32 return Err(PoolsimError::invalid_input(
33 "INVALID_SERVER_COUNT",
34 "server count must be > 0",
35 None,
36 ));
37 }
38 if mu <= 0.0 {
39 return Err(PoolsimError::invalid_input(
40 "INVALID_SERVICE_RATE",
41 "service rate must be > 0",
42 None,
43 ));
44 }
45 if lambda <= 0.0 {
46 return Ok(0.0);
47 }
48
49 let rho = utilisation(lambda, mu, c);
50 if rho >= 1.0 {
51 return Err(PoolsimError::Saturated { rho });
52 }
53
54 let offered_load = lambda / mu;
55 let mut sum = 1.0;
56 let mut term = 1.0;
57
58 for k in 1..c {
59 term *= offered_load / k as f64;
60 sum += term;
61 }
62
63 let term_c = term * offered_load / c as f64;
64 let top = term_c / (1.0 - rho);
65 Ok(top / (sum + top))
66}
67
68pub fn mean_queue_wait_ms(lambda: f64, mu: f64, c: u32) -> Result<f64, PoolsimError> {
75 if lambda <= 0.0 {
76 return Ok(0.0);
77 }
78
79 let p_wait = erlang_c(lambda, mu, c)?;
80 let denom = c as f64 * mu - lambda;
81 if !denom.is_finite() || denom <= 0.0 {
82 return Err(PoolsimError::Saturated {
83 rho: utilisation(lambda, mu, c),
84 });
85 }
86
87 Ok((p_wait / denom) * 1_000.0)
88}
89
90pub fn queue_wait_percentile_ms(
99 lambda: f64,
100 mu: f64,
101 c: u32,
102 quantile: f64,
103) -> Result<f64, PoolsimError> {
104 if lambda <= 0.0 {
105 return Ok(0.0);
106 }
107
108 let q = quantile.clamp(0.0, 1.0);
109 if q == 0.0 {
110 return Ok(0.0);
111 }
112
113 let p_wait = erlang_c(lambda, mu, c)?;
114 if q <= 1.0 - p_wait {
115 return Ok(0.0);
116 }
117
118 let rate = c as f64 * mu - lambda;
119 if !rate.is_finite() || rate <= 0.0 {
120 return Err(PoolsimError::Saturated {
121 rho: utilisation(lambda, mu, c),
122 });
123 }
124
125 let tail = ((1.0 - q) / p_wait).max(f64::MIN_POSITIVE);
126 Ok((-tail.ln() / rate) * 1_000.0)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn erlang_c_known_case() {
135 let c = 10;
136 let mu = 1.0;
137 let lambda = 8.0;
138 let p_wait = erlang_c(lambda, mu, c).expect("valid erlang c");
139 assert!((p_wait - 0.40918).abs() < 0.005);
140 }
141
142 #[test]
143 fn erlang_c_reference_matrix() {
144 let mu = 1.0;
145 let cases = [
146 (2, 0.5, 0.33333333),
147 (2, 0.8, 0.71111111),
148 (2, 0.9, 0.85263158),
149 (3, 0.7, 0.49234450),
150 (3, 0.9, 0.81706102),
151 (4, 0.5, 0.17391304),
152 (4, 0.8, 0.59643247),
153 (4, 0.95, 0.89141900),
154 (5, 0.7, 0.37783823),
155 (5, 0.9, 0.76249322),
156 (6, 0.8, 0.51777200),
157 (6, 0.95, 0.86558880),
158 (8, 0.7, 0.27060293),
159 (8, 0.9, 0.70153299),
160 (10, 0.8, 0.40918015),
161 (10, 0.95, 0.82558558),
162 (12, 0.7, 0.18388863),
163 (12, 0.9, 0.64004291),
164 (16, 0.8, 0.30488391),
165 (20, 0.9, 0.55076900),
166 ];
167
168 for (c, rho, expected) in cases {
169 let lambda = rho * c as f64 * mu;
170 let actual = erlang_c(lambda, mu, c).expect("reference case should be valid");
171 assert!(
172 (actual - expected).abs() < 1e-6,
173 "c={c}, rho={rho}, expected={expected}, actual={actual}"
174 );
175 }
176 }
177
178 #[test]
179 fn mean_queue_wait_increases_as_utilisation_rises() {
180 let c = 8;
181 let mu = 1.0;
182 let low =
183 mean_queue_wait_ms(0.5 * c as f64 * mu, mu, c).expect("low utilisation should work");
184 let high =
185 mean_queue_wait_ms(0.9 * c as f64 * mu, mu, c).expect("high utilisation should work");
186 assert!(high > low);
187 }
188
189 #[test]
190 fn queue_percentile_is_zero_when_quantile_in_non_waiting_mass() {
191 let c = 4;
192 let mu = 1.0;
193 let lambda = 0.5 * c as f64 * mu;
194 let p_wait = erlang_c(lambda, mu, c).expect("valid erlang c");
195 let threshold = 1.0 - p_wait;
196 let q = threshold * 0.99;
197 let value = queue_wait_percentile_ms(lambda, mu, c, q).expect("valid percentile");
198 assert_eq!(value, 0.0);
199 }
200
201 #[test]
202 fn nan_service_rate_maps_to_saturated_in_wait_metrics() {
203 let err = mean_queue_wait_ms(1.0, f64::NAN, 2).expect_err("nan service rate should fail");
204 assert_eq!(err.code(), "SATURATED");
205
206 let err = queue_wait_percentile_ms(1.0, f64::NAN, 2, 0.99)
207 .expect_err("nan service rate should fail");
208 assert_eq!(err.code(), "SATURATED");
209 }
210}