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(lambda: f64, mu: f64, c: u32, quantile: f64) -> Result<f64, PoolsimError> {
99 if lambda <= 0.0 {
100 return Ok(0.0);
101 }
102
103 let q = quantile.clamp(0.0, 1.0);
104 if q == 0.0 {
105 return Ok(0.0);
106 }
107
108 let p_wait = erlang_c(lambda, mu, c)?;
109 if q <= 1.0 - p_wait {
110 return Ok(0.0);
111 }
112
113 let rate = c as f64 * mu - lambda;
114 if !rate.is_finite() || rate <= 0.0 {
115 return Err(PoolsimError::Saturated {
116 rho: utilisation(lambda, mu, c),
117 });
118 }
119
120 let tail = ((1.0 - q) / p_wait).max(f64::MIN_POSITIVE);
121 Ok((-tail.ln() / rate) * 1_000.0)
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn erlang_c_known_case() {
130 let c = 10;
131 let mu = 1.0;
132 let lambda = 8.0;
133 let p_wait = erlang_c(lambda, mu, c).expect("valid erlang c");
134 assert!((p_wait - 0.40918).abs() < 0.005);
135 }
136
137 #[test]
138 fn erlang_c_reference_matrix() {
139 let mu = 1.0;
140 let cases = [
141 (2, 0.5, 0.33333333),
142 (2, 0.8, 0.71111111),
143 (2, 0.9, 0.85263158),
144 (3, 0.7, 0.49234450),
145 (3, 0.9, 0.81706102),
146 (4, 0.5, 0.17391304),
147 (4, 0.8, 0.59643247),
148 (4, 0.95, 0.89141900),
149 (5, 0.7, 0.37783823),
150 (5, 0.9, 0.76249322),
151 (6, 0.8, 0.51777200),
152 (6, 0.95, 0.86558880),
153 (8, 0.7, 0.27060293),
154 (8, 0.9, 0.70153299),
155 (10, 0.8, 0.40918015),
156 (10, 0.95, 0.82558558),
157 (12, 0.7, 0.18388863),
158 (12, 0.9, 0.64004291),
159 (16, 0.8, 0.30488391),
160 (20, 0.9, 0.55076900),
161 ];
162
163 for (c, rho, expected) in cases {
164 let lambda = rho * c as f64 * mu;
165 let actual = erlang_c(lambda, mu, c).expect("reference case should be valid");
166 assert!(
167 (actual - expected).abs() < 1e-6,
168 "c={c}, rho={rho}, expected={expected}, actual={actual}"
169 );
170 }
171 }
172
173 #[test]
174 fn mean_queue_wait_increases_as_utilisation_rises() {
175 let c = 8;
176 let mu = 1.0;
177 let low = mean_queue_wait_ms(0.5 * c as f64 * mu, mu, c).expect("low utilisation should work");
178 let high = mean_queue_wait_ms(0.9 * c as f64 * mu, mu, c).expect("high utilisation should work");
179 assert!(high > low);
180 }
181
182 #[test]
183 fn queue_percentile_is_zero_when_quantile_in_non_waiting_mass() {
184 let c = 4;
185 let mu = 1.0;
186 let lambda = 0.5 * c as f64 * mu;
187 let p_wait = erlang_c(lambda, mu, c).expect("valid erlang c");
188 let threshold = 1.0 - p_wait;
189 let q = threshold * 0.99;
190 let value = queue_wait_percentile_ms(lambda, mu, c, q).expect("valid percentile");
191 assert_eq!(value, 0.0);
192 }
193
194 #[test]
195 fn nan_service_rate_maps_to_saturated_in_wait_metrics() {
196 let err = mean_queue_wait_ms(1.0, f64::NAN, 2).expect_err("nan service rate should fail");
197 assert_eq!(err.code(), "SATURATED");
198
199 let err = queue_wait_percentile_ms(1.0, f64::NAN, 2, 0.99).expect_err("nan service rate should fail");
200 assert_eq!(err.code(), "SATURATED");
201 }
202}