tokio_prompt_orchestrator/
admission_control.rs1use std::sync::atomic::{AtomicUsize, Ordering};
37use std::sync::{Arc, Mutex};
38use std::time::{Duration, Instant};
39
40#[derive(Debug, Clone)]
44pub struct AdmissionConfig {
45 pub min_limit: usize,
47 pub max_limit: usize,
49 pub additive_increase: usize,
51 pub multiplicative_decrease: f64,
53 pub gradient_threshold: f64,
56 pub latency_ewma_alpha: f64,
58}
59
60impl Default for AdmissionConfig {
61 fn default() -> Self {
62 Self {
63 min_limit: 4,
64 max_limit: 512,
65 additive_increase: 1,
66 multiplicative_decrease: 0.9,
67 gradient_threshold: 2.0,
68 latency_ewma_alpha: 0.1,
69 }
70 }
71}
72
73struct AcceptanceWindow {
77 buf: [bool; 100],
78 head: usize,
79 filled: bool,
80}
81
82impl AcceptanceWindow {
83 fn new() -> Self {
84 Self {
85 buf: [false; 100],
86 head: 0,
87 filled: false,
88 }
89 }
90
91 fn record(&mut self, accepted: bool) {
92 self.buf[self.head] = accepted;
93 self.head = (self.head + 1) % 100;
94 if self.head == 0 {
95 self.filled = true;
96 }
97 }
98
99 fn rate(&self) -> f64 {
100 let n = if self.filled { 100 } else { self.head };
101 if n == 0 {
102 return 1.0;
103 }
104 let accepted = self.buf[..n].iter().filter(|&&b| b).count();
105 accepted as f64 / n as f64
106 }
107}
108
109struct Inner {
112 limit_snapshot: usize,
114 latency_ewma_ns: f64,
116 acceptance: AcceptanceWindow,
118}
119
120pub struct AdmissionController {
124 config: AdmissionConfig,
125 limit: AtomicUsize,
127 in_flight: Arc<AtomicUsize>,
129 inner: Mutex<Inner>,
130}
131
132impl AdmissionController {
133 pub fn new(config: AdmissionConfig) -> Arc<Self> {
135 let start = config.min_limit;
136 Arc::new(Self {
137 config: config.clone(),
138 limit: AtomicUsize::new(start),
139 in_flight: Arc::new(AtomicUsize::new(0)),
140 inner: Mutex::new(Inner {
141 limit_snapshot: start,
142 latency_ewma_ns: 0.0,
143 acceptance: AcceptanceWindow::new(),
144 }),
145 })
146 }
147
148 pub fn try_acquire(self: &Arc<Self>) -> Option<AcquireGuard> {
153 let limit = self.limit.load(Ordering::Relaxed);
154 let prev = self.in_flight.fetch_add(1, Ordering::AcqRel);
156 if prev < limit {
157 let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
158 g.acceptance.record(true);
159 drop(g);
160 Some(AcquireGuard {
161 ctrl: Arc::clone(self),
162 start: Instant::now(),
163 signalled: false,
164 })
165 } else {
166 self.in_flight.fetch_sub(1, Ordering::AcqRel);
168 let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
169 g.acceptance.record(false);
170 None
171 }
172 }
173
174 pub fn on_success(&self) {
177 let current = self.limit.load(Ordering::Relaxed);
178 let next = (current + self.config.additive_increase)
179 .min(self.config.max_limit);
180 self.limit.store(next, Ordering::Relaxed);
181 let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
182 g.limit_snapshot = next;
183 }
184
185 pub fn on_overload(&self) {
188 let current = self.limit.load(Ordering::Relaxed);
189 let next = ((current as f64 * self.config.multiplicative_decrease)
190 .floor() as usize)
191 .max(self.config.min_limit);
192 self.limit.store(next, Ordering::Relaxed);
193 let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
194 g.limit_snapshot = next;
195 }
196
197 fn record_latency(&self, latency: Duration) {
202 let ns = latency.as_nanos() as f64;
203 let mut g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
204 if g.latency_ewma_ns == 0.0 {
205 g.latency_ewma_ns = ns;
206 } else {
207 let alpha = self.config.latency_ewma_alpha;
208 g.latency_ewma_ns = alpha * ns + (1.0 - alpha) * g.latency_ewma_ns;
209 }
210 let ewma = g.latency_ewma_ns;
211 drop(g);
212
213 if ewma > 0.0 && ns / ewma > self.config.gradient_threshold {
214 self.on_overload();
215 } else {
216 self.on_success();
217 }
218 }
219
220 pub fn stats(&self) -> AdmissionStats {
222 let limit = self.limit.load(Ordering::Relaxed);
223 let in_flight = self.in_flight.load(Ordering::Relaxed);
224 let g = self.inner.lock().unwrap_or_else(|e| e.into_inner());
225 AdmissionStats {
226 current_limit: limit,
227 in_flight,
228 acceptance_rate: g.acceptance.rate(),
229 }
230 }
231
232 pub fn current_limit(&self) -> usize {
234 self.limit.load(Ordering::Relaxed)
235 }
236
237 pub fn in_flight(&self) -> usize {
239 self.in_flight.load(Ordering::Relaxed)
240 }
241}
242
243pub struct AcquireGuard {
251 ctrl: Arc<AdmissionController>,
252 start: Instant,
253 signalled: bool,
254}
255
256impl AcquireGuard {
257 pub fn success(mut self) {
260 self.signalled = true;
261 let latency = self.start.elapsed();
262 self.ctrl.record_latency(latency);
263 }
265}
266
267impl Drop for AcquireGuard {
268 fn drop(&mut self) {
269 self.ctrl.in_flight.fetch_sub(1, Ordering::AcqRel);
270 }
271}
272
273#[derive(Debug, Clone)]
277pub struct AdmissionStats {
278 pub current_limit: usize,
280 pub in_flight: usize,
282 pub acceptance_rate: f64,
284}
285
286#[cfg(test)]
289mod tests {
290 use super::*;
291 use std::sync::atomic::AtomicBool;
292 use std::thread;
293
294 #[test]
295 fn try_acquire_within_limit() {
296 let ctrl = AdmissionController::new(AdmissionConfig {
297 min_limit: 2,
298 max_limit: 10,
299 ..Default::default()
300 });
301 let g1 = ctrl.try_acquire().expect("slot 1");
302 let g2 = ctrl.try_acquire().expect("slot 2");
303 assert!(ctrl.try_acquire().is_none(), "limit reached");
304 drop(g1);
305 assert!(ctrl.try_acquire().is_some(), "slot freed");
306 drop(g2);
307 }
308
309 #[test]
310 fn on_success_increases_limit() {
311 let ctrl = AdmissionController::new(AdmissionConfig {
312 min_limit: 4,
313 max_limit: 10,
314 additive_increase: 2,
315 ..Default::default()
316 });
317 let start = ctrl.current_limit();
318 ctrl.on_success();
319 assert_eq!(ctrl.current_limit(), start + 2);
320 }
321
322 #[test]
323 fn on_overload_decreases_limit() {
324 let ctrl = AdmissionController::new(AdmissionConfig {
325 min_limit: 4,
326 max_limit: 100,
327 multiplicative_decrease: 0.5,
328 ..Default::default()
329 });
330 ctrl.limit.store(20, Ordering::Relaxed);
332 ctrl.on_overload();
333 assert_eq!(ctrl.current_limit(), 10);
334 }
335
336 #[test]
337 fn limit_capped_at_max() {
338 let ctrl = AdmissionController::new(AdmissionConfig {
339 min_limit: 4,
340 max_limit: 5,
341 additive_increase: 10,
342 ..Default::default()
343 });
344 ctrl.on_success();
345 assert_eq!(ctrl.current_limit(), 5);
346 }
347
348 #[test]
349 fn limit_floored_at_min() {
350 let ctrl = AdmissionController::new(AdmissionConfig {
351 min_limit: 4,
352 max_limit: 100,
353 multiplicative_decrease: 0.01,
354 ..Default::default()
355 });
356 ctrl.on_overload();
357 assert_eq!(ctrl.current_limit(), 4);
358 }
359
360 #[test]
361 fn in_flight_decrements_on_drop() {
362 let ctrl = AdmissionController::new(AdmissionConfig {
363 min_limit: 4,
364 max_limit: 100,
365 ..Default::default()
366 });
367 {
368 let _g = ctrl.try_acquire().unwrap();
369 assert_eq!(ctrl.in_flight(), 1);
370 }
371 assert_eq!(ctrl.in_flight(), 0);
372 }
373
374 #[test]
375 fn acceptance_rate_tracks_rejections() {
376 let ctrl = AdmissionController::new(AdmissionConfig {
377 min_limit: 1,
378 max_limit: 1,
379 ..Default::default()
380 });
381 let g = ctrl.try_acquire().unwrap();
383 for _ in 0..9 {
384 let _ = ctrl.try_acquire(); }
386 drop(g);
387 let stats = ctrl.stats();
388 assert!(stats.acceptance_rate < 0.5);
390 }
391
392 #[test]
393 fn concurrent_acquire_release() {
394 let ctrl = AdmissionController::new(AdmissionConfig {
395 min_limit: 8,
396 max_limit: 8,
397 ..Default::default()
398 });
399 let success = Arc::new(AtomicBool::new(true));
400 let mut handles = Vec::new();
401 for _ in 0..8 {
402 let ctrl2 = Arc::clone(&ctrl);
403 let ok = Arc::clone(&success);
404 handles.push(thread::spawn(move || {
405 if let Some(g) = ctrl2.try_acquire() {
406 thread::sleep(Duration::from_millis(5));
407 g.success();
408 } else {
409 ok.store(false, Ordering::Relaxed);
410 }
411 }));
412 }
413 for h in handles {
414 h.join().expect("thread panicked");
415 }
416 assert!(success.load(Ordering::Relaxed));
417 assert_eq!(ctrl.in_flight(), 0);
418 }
419
420 #[test]
421 fn aimd_convergence_under_normal_load() {
422 let ctrl = AdmissionController::new(AdmissionConfig {
424 min_limit: 4,
425 max_limit: 20,
426 additive_increase: 1,
427 multiplicative_decrease: 0.9,
428 gradient_threshold: 10.0, latency_ewma_alpha: 0.5,
430 ..Default::default()
431 });
432 for _ in 0..20 {
433 if let Some(g) = ctrl.try_acquire() {
434 g.success();
435 } else {
436 ctrl.on_success();
437 }
438 }
439 assert!(ctrl.current_limit() > 4);
441 }
442}