1use serde::{Deserialize, Serialize};
14
15use super::protocol::{BandConfig, DRAIN_SUSPECT_FRACTION};
16
17#[derive(Debug, Clone)]
19pub struct WindowController {
20 min_samples: usize,
21 min_wall_s: f64,
22 issued: usize,
23 closed_at_s: Option<f64>,
24 last_completion_s: f64,
25 in_flight: usize,
26 peak_in_flight: usize,
27}
28
29impl WindowController {
30 #[must_use]
33 pub fn new(config: &BandConfig) -> Self {
34 Self {
35 min_samples: config.min_samples,
36 min_wall_s: config.min_wall_clock.as_secs_f64(),
37 issued: 0,
38 closed_at_s: None,
39 last_completion_s: 0.0,
40 in_flight: 0,
41 peak_in_flight: 0,
42 }
43 }
44
45 #[must_use]
48 pub fn with_bounds(min_samples: usize, min_wall_s: f64) -> Self {
49 Self {
50 min_samples,
51 min_wall_s,
52 issued: 0,
53 closed_at_s: None,
54 last_completion_s: 0.0,
55 in_flight: 0,
56 peak_in_flight: 0,
57 }
58 }
59
60 pub fn try_admit(&mut self, now_s: f64) -> Option<usize> {
66 self.try_admit_with_in_flight(now_s).map(|(index, _)| index)
67 }
68
69 pub fn try_admit_with_in_flight(&mut self, now_s: f64) -> Option<(usize, usize)> {
76 if self.closed_at_s.is_some() {
77 return None;
78 }
79 if self.issued >= self.min_samples && now_s >= self.min_wall_s {
80 self.closed_at_s = Some(now_s);
81 return None;
82 }
83 let index = self.issued;
84 self.issued += 1;
85 self.in_flight += 1;
86 self.peak_in_flight = self.peak_in_flight.max(self.in_flight);
87 Some((index, self.in_flight))
88 }
89
90 pub fn complete(&mut self, now_s: f64) -> bool {
93 self.in_flight = self.in_flight.saturating_sub(1);
94 self.last_completion_s = self.last_completion_s.max(now_s);
95 self.closed_at_s.is_some_and(|t| now_s > t)
96 }
97
98 #[must_use]
102 pub fn peak_in_flight(&self) -> usize {
103 self.peak_in_flight
104 }
105
106 #[must_use]
108 pub fn issued(&self) -> usize {
109 self.issued
110 }
111
112 #[must_use]
114 pub fn in_flight(&self) -> usize {
115 self.in_flight
116 }
117
118 #[must_use]
120 pub fn is_closed(&self) -> bool {
121 self.closed_at_s.is_some()
122 }
123
124 #[must_use]
128 pub fn report(&self) -> WindowReport {
129 let close = self.closed_at_s.unwrap_or(self.last_completion_s);
130 let drain_ms = ((self.last_completion_s - close).max(0.0)) * 1000.0;
131 let window_ms = close * 1000.0;
132 let mut suspect = Vec::new();
133 if window_ms > 0.0 && drain_ms > DRAIN_SUSPECT_FRACTION * window_ms {
134 suspect.push(format!(
135 "§4.4.7 drain_ms={drain_ms:.1} > 0.5 x window_ms={window_ms:.1}: one request \
136 dominated the window; re-run this band with a longer window"
137 ));
138 }
139 if self.closed_at_s.is_none() {
140 suspect.push(
141 "§4.4.2 the window never closed: neither termination bound was reached, so this \
142 band did not run the protocol"
143 .to_string(),
144 );
145 }
146 WindowReport {
147 requested: self.issued,
148 window_ms,
149 drain_ms,
150 client_peak_in_flight: self.peak_in_flight,
151 suspect,
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[serde(deny_unknown_fields)]
159pub struct WindowReport {
160 pub requested: usize,
162 pub window_ms: f64,
164 pub drain_ms: f64,
166 pub client_peak_in_flight: usize,
168 pub suspect: Vec<String>,
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use std::time::Duration;
176
177 #[test]
180 fn sample_bound_alone_does_not_close_the_window() {
181 let mut w = WindowController::with_bounds(3, 60.0);
182 for i in 0..3 {
183 assert_eq!(w.try_admit(0.1 * f64::from(i)), Some(i as usize));
184 }
185 assert_eq!(w.try_admit(0.4), Some(3), "still open: 0.4s < 60s");
186 assert!(!w.is_closed());
187 }
188
189 #[test]
192 fn wall_clock_bound_alone_does_not_close_the_window() {
193 let mut w = WindowController::with_bounds(30, 1.0);
194 for _ in 0..4 {
195 assert!(
196 w.try_admit(100.0).is_some(),
197 "still open: only 4 of 30 samples"
198 );
199 }
200 assert!(!w.is_closed());
201 }
202
203 #[test]
204 fn window_closes_only_when_both_bounds_are_satisfied() {
205 let mut w = WindowController::with_bounds(3, 10.0);
206 assert!(w.try_admit(0.0).is_some());
207 assert!(w.try_admit(1.0).is_some());
208 assert!(w.try_admit(2.0).is_some());
209 assert!(!w.is_closed());
210 assert_eq!(w.try_admit(10.0), None, "3 samples AND 10s -> closed");
211 assert!(w.is_closed());
212 }
213
214 #[test]
217 fn no_new_request_is_admitted_at_or_after_t() {
218 let mut w = WindowController::with_bounds(1, 1.0);
219 assert!(w.try_admit(0.0).is_some());
220 assert_eq!(w.try_admit(1.0), None);
221 assert_eq!(w.try_admit(1.0001), None);
222 assert_eq!(w.try_admit(500.0), None);
223 assert_eq!(w.issued(), 1, "exactly one request was ever admitted");
224 }
225
226 #[test]
228 fn drain_ms_is_measured_from_window_close() {
229 let mut w = WindowController::with_bounds(2, 4.0);
230 assert!(w.try_admit(0.0).is_some());
231 assert!(w.try_admit(1.0).is_some());
232 assert!(!w.complete(2.0), "completed inside the window");
233 assert_eq!(w.try_admit(4.0), None, "T = 4.0");
234 assert!(w.complete(5.5), "completed during the drain");
235 let r = w.report();
236 assert!((r.window_ms - 4000.0).abs() < 1e-9, "{}", r.window_ms);
237 assert!((r.drain_ms - 1500.0).abs() < 1e-9, "{}", r.drain_ms);
238 assert!(r.suspect.is_empty(), "{:?}", r.suspect);
239 }
240
241 #[test]
243 fn a_dominating_request_is_annotated_suspect() {
244 let mut w = WindowController::with_bounds(1, 2.0);
245 assert!(w.try_admit(0.0).is_some());
246 assert_eq!(w.try_admit(2.0), None);
247 assert!(w.complete(20.0));
248 let r = w.report();
249 assert!((r.drain_ms - 18000.0).abs() < 1e-9);
250 assert_eq!(r.suspect.len(), 1, "{:?}", r.suspect);
251 assert!(r.suspect[0].contains("drain_ms"));
252 }
253
254 #[test]
257 fn an_unclosed_window_is_suspect_not_clean() {
258 let mut w = WindowController::with_bounds(100, 60.0);
259 assert!(w.try_admit(0.0).is_some());
260 assert!(!w.complete(1.0));
261 let r = w.report();
262 assert_eq!(r.drain_ms, 0.0);
263 assert!(
264 r.suspect.iter().any(|s| s.contains("never closed")),
265 "{:?}",
266 r.suspect
267 );
268 }
269
270 #[test]
273 fn peak_in_flight_tracks_concurrent_admissions() {
274 let mut w = WindowController::with_bounds(100, 100.0);
275 for _ in 0..8 {
276 assert!(w.try_admit(0.0).is_some());
277 }
278 assert_eq!(w.in_flight(), 8);
279 assert_eq!(w.peak_in_flight(), 8);
280 for _ in 0..8 {
281 w.complete(1.0);
282 }
283 assert_eq!(w.in_flight(), 0);
284 assert_eq!(w.peak_in_flight(), 8, "peak is a high-water mark");
285 }
286
287 #[test]
288 fn controller_reads_its_bounds_from_the_band_config() {
289 let cfg = BandConfig::conformant(8);
290 let w = WindowController::new(&cfg);
291 assert_eq!(w.min_samples, 64, "max(30, 8*8)");
292 assert!((w.min_wall_s - 60.0).abs() < 1e-9);
293 assert_eq!(cfg.quiesce, Duration::from_secs(5));
294 }
295}
296
297#[must_use]
310pub fn closed_loop_probe(
311 concurrency: usize,
312 requests: usize,
313 work: std::time::Duration,
314) -> (usize, std::time::Duration) {
315 use std::sync::Mutex;
316 use std::time::Instant;
317
318 let controller = Mutex::new(WindowController::with_bounds(requests, 0.0));
319 let origin = Instant::now();
320 std::thread::scope(|scope| {
321 for _ in 0..concurrency {
322 scope.spawn(|| loop {
323 let admitted = {
324 let mut c = controller
325 .lock()
326 .unwrap_or_else(std::sync::PoisonError::into_inner);
327 c.try_admit(origin.elapsed().as_secs_f64())
328 };
329 if admitted.is_none() {
330 break;
331 }
332 std::thread::sleep(work);
333 let mut c = controller
334 .lock()
335 .unwrap_or_else(std::sync::PoisonError::into_inner);
336 c.complete(origin.elapsed().as_secs_f64());
337 });
338 }
339 });
340 let peak = controller
341 .lock()
342 .unwrap_or_else(std::sync::PoisonError::into_inner)
343 .peak_in_flight();
344 (peak, origin.elapsed())
345}
346
347#[cfg(test)]
348mod concurrency_proof_tests {
349 use super::*;
350 use std::time::Duration;
351
352 #[test]
354 fn eight_workers_are_actually_concurrent() {
355 let (peak, _) = closed_loop_probe(8, 64, Duration::from_millis(20));
356 assert_eq!(
357 peak, 8,
358 "peak in-flight must reach c; a serialising client gives 1"
359 );
360 }
361
362 #[test]
369 fn wall_time_at_c8_is_not_eight_times_the_c1_time() {
370 let requests = 32;
371 let work = Duration::from_millis(20);
372 let (peak1, wall1) = closed_loop_probe(1, requests, work);
373 let (peak8, wall8) = closed_loop_probe(8, requests, work);
374
375 assert_eq!(peak1, 1);
376 assert_eq!(peak8, 8);
377 let speedup = wall1.as_secs_f64() / wall8.as_secs_f64();
378 eprintln!(
379 "closed_loop_probe: {requests} requests x {work:?} -- \
380 c=1 peak={peak1} wall={wall1:?}; c=8 peak={peak8} wall={wall8:?}; speedup={speedup:.2}x"
381 );
382 assert!(
383 speedup > 4.0,
384 "c=1 took {wall1:?}, c=8 took {wall8:?} (speedup {speedup:.2}x); \
385 a concurrent client must be much faster, a sequential one scores ~1.0x"
386 );
387 }
388
389 #[test]
391 fn the_probe_reports_one_for_a_sequential_client() {
392 let (peak, wall) = closed_loop_probe(1, 8, Duration::from_millis(10));
393 assert_eq!(peak, 1);
394 assert!(
395 wall >= Duration::from_millis(70),
396 "8 x 10ms sequential must take ~80ms, got {wall:?}"
397 );
398 }
399}