1use std::collections::VecDeque;
7use std::time::Instant;
8
9use a3s_box_core::config::ScalingPolicy;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ScaleDecision {
14 ScaleUp(usize),
16 ScaleDown(usize),
18 Hold,
20}
21
22struct PressureWindow {
24 events: VecDeque<(Instant, bool)>,
26 window: std::time::Duration,
28}
29
30impl PressureWindow {
31 fn new(window_secs: u64) -> Self {
32 Self {
33 events: VecDeque::new(),
34 window: std::time::Duration::from_secs(window_secs),
35 }
36 }
37
38 fn record(&mut self, hit: bool) {
40 self.events.push_back((Instant::now(), hit));
41 self.prune();
42 }
43
44 #[cfg(test)]
46 fn record_at(&mut self, at: Instant, hit: bool) {
47 self.events.push_back((at, hit));
48 self.prune();
49 }
50
51 fn prune(&mut self) {
53 let cutoff = Instant::now()
54 .checked_sub(self.window)
55 .unwrap_or_else(Instant::now);
56 while let Some(&(ts, _)) = self.events.front() {
57 if ts < cutoff {
58 self.events.pop_front();
59 } else {
60 break;
61 }
62 }
63 }
64
65 fn miss_rate(&mut self) -> Option<f64> {
67 self.prune();
68 if self.events.is_empty() {
69 return None;
70 }
71 let total = self.events.len() as f64;
72 let misses = self.events.iter().filter(|(_, hit)| !hit).count() as f64;
73 Some(misses / total)
74 }
75
76 fn len(&self) -> usize {
78 self.events.len()
79 }
80}
81
82pub struct PoolScaler {
84 policy: ScalingPolicy,
86 window: PressureWindow,
88 last_scale_at: Option<Instant>,
90 current_min_idle: usize,
92 max_min_idle: usize,
94 gateway_pressure: f64,
96 gateway_pressure_at: Option<Instant>,
98}
99
100impl PoolScaler {
101 pub fn new(policy: ScalingPolicy, initial_min_idle: usize, max_size: usize) -> Self {
103 let max_min_idle = if policy.max_min_idle > 0 {
104 policy.max_min_idle.min(max_size)
105 } else {
106 max_size
107 };
108
109 Self {
110 window: PressureWindow::new(policy.window_secs),
111 policy,
112 last_scale_at: None,
113 current_min_idle: initial_min_idle,
114 max_min_idle,
115 gateway_pressure: 0.0,
116 gateway_pressure_at: None,
117 }
118 }
119
120 pub fn record_acquire(&mut self, hit: bool) {
122 self.window.record(hit);
123 }
124
125 pub fn current_min_idle(&self) -> usize {
127 self.current_min_idle
128 }
129
130 pub fn update_gateway_pressure(&mut self, pressure: f64) {
138 self.gateway_pressure = pressure.clamp(0.0, 1.0);
139 self.gateway_pressure_at = Some(Instant::now());
140 }
141
142 pub fn gateway_pressure(&self) -> f64 {
144 self.gateway_pressure
145 }
146
147 fn is_gateway_pressure_fresh(&self) -> bool {
149 match self.gateway_pressure_at {
150 Some(at) => at.elapsed().as_secs() < self.policy.window_secs * 2,
151 None => false,
152 }
153 }
154
155 fn effective_miss_rate(&mut self) -> Option<f64> {
163 let local = self.window.miss_rate()?;
164
165 if self.is_gateway_pressure_fresh() && self.gateway_pressure > 0.0 {
166 Some(local * 0.6 + self.gateway_pressure * 0.4)
167 } else {
168 Some(local)
169 }
170 }
171
172 pub fn evaluate(&mut self) -> ScaleDecision {
176 if let Some(last) = self.last_scale_at {
178 if last.elapsed().as_secs() < self.policy.cooldown_secs {
179 return ScaleDecision::Hold;
180 }
181 }
182
183 if self.window.len() < 3 {
185 return ScaleDecision::Hold;
186 }
187
188 let miss_rate = match self.effective_miss_rate() {
189 Some(rate) => rate,
190 None => return ScaleDecision::Hold,
191 };
192
193 if miss_rate > self.policy.scale_up_threshold {
194 let new_min = (self.current_min_idle + 1).min(self.max_min_idle);
196 if new_min > self.current_min_idle {
197 self.current_min_idle = new_min;
198 self.last_scale_at = Some(Instant::now());
199 ScaleDecision::ScaleUp(1)
200 } else {
201 ScaleDecision::Hold }
203 } else if miss_rate < self.policy.scale_down_threshold {
204 let new_min = self.current_min_idle.saturating_sub(1).max(1);
206 if new_min < self.current_min_idle {
207 self.current_min_idle = new_min;
208 self.last_scale_at = Some(Instant::now());
209 ScaleDecision::ScaleDown(1)
210 } else {
211 ScaleDecision::Hold }
213 } else {
214 ScaleDecision::Hold
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn default_policy() -> ScalingPolicy {
224 ScalingPolicy {
225 enabled: true,
226 scale_up_threshold: 0.3,
227 scale_down_threshold: 0.05,
228 max_min_idle: 0, cooldown_secs: 60,
230 window_secs: 120,
231 }
232 }
233
234 #[test]
237 fn test_window_empty_miss_rate_is_none() {
238 let mut w = PressureWindow::new(120);
239 assert_eq!(w.miss_rate(), None);
240 assert_eq!(w.len(), 0);
241 }
242
243 #[test]
244 fn test_window_all_hits() {
245 let mut w = PressureWindow::new(120);
246 for _ in 0..10 {
247 w.record(true);
248 }
249 assert_eq!(w.miss_rate(), Some(0.0));
250 }
251
252 #[test]
253 fn test_window_all_misses() {
254 let mut w = PressureWindow::new(120);
255 for _ in 0..10 {
256 w.record(false);
257 }
258 assert_eq!(w.miss_rate(), Some(1.0));
259 }
260
261 #[test]
262 fn test_window_mixed_events() {
263 let mut w = PressureWindow::new(120);
264 for _ in 0..3 {
266 w.record(false);
267 }
268 for _ in 0..7 {
269 w.record(true);
270 }
271 let rate = w.miss_rate().unwrap();
272 assert!((rate - 0.3).abs() < 0.001);
273 }
274
275 #[test]
276 fn test_window_prunes_old_events() {
277 let mut w = PressureWindow::new(1);
279 let old = Instant::now() - std::time::Duration::from_secs(5);
280 w.record_at(old, false);
281 w.record_at(old, false);
282 w.record(true);
284 assert_eq!(w.len(), 1);
285 assert_eq!(w.miss_rate(), Some(0.0));
286 }
287
288 #[test]
289 fn test_window_record_at() {
290 let mut w = PressureWindow::new(300);
291 let now = Instant::now();
292 w.record_at(now, true);
293 w.record_at(now, false);
294 assert_eq!(w.len(), 2);
295 assert_eq!(w.miss_rate(), Some(0.5));
296 }
297
298 #[test]
301 fn test_scale_decision_equality() {
302 assert_eq!(ScaleDecision::Hold, ScaleDecision::Hold);
303 assert_eq!(ScaleDecision::ScaleUp(1), ScaleDecision::ScaleUp(1));
304 assert_eq!(ScaleDecision::ScaleDown(1), ScaleDecision::ScaleDown(1));
305 assert_ne!(ScaleDecision::ScaleUp(1), ScaleDecision::ScaleDown(1));
306 }
307
308 #[test]
309 fn test_scale_decision_debug() {
310 let d = ScaleDecision::ScaleUp(2);
311 assert!(format!("{:?}", d).contains("ScaleUp"));
312 }
313
314 #[test]
317 fn test_scaler_initial_min_idle() {
318 let scaler = PoolScaler::new(default_policy(), 2, 10);
319 assert_eq!(scaler.current_min_idle(), 2);
320 }
321
322 #[test]
323 fn test_scaler_max_min_idle_defaults_to_max_size() {
324 let scaler = PoolScaler::new(default_policy(), 2, 10);
325 assert_eq!(scaler.max_min_idle, 10);
326 }
327
328 #[test]
329 fn test_scaler_max_min_idle_capped_by_max_size() {
330 let mut policy = default_policy();
331 policy.max_min_idle = 20; let scaler = PoolScaler::new(policy, 2, 10);
333 assert_eq!(scaler.max_min_idle, 10); }
335
336 #[test]
337 fn test_scaler_max_min_idle_explicit() {
338 let mut policy = default_policy();
339 policy.max_min_idle = 5;
340 let scaler = PoolScaler::new(policy, 2, 10);
341 assert_eq!(scaler.max_min_idle, 5);
342 }
343
344 #[test]
345 fn test_scaler_hold_with_few_events() {
346 let mut scaler = PoolScaler::new(default_policy(), 2, 10);
347 scaler.record_acquire(false);
348 scaler.record_acquire(false);
349 assert_eq!(scaler.evaluate(), ScaleDecision::Hold);
351 }
352
353 #[test]
354 fn test_scaler_scale_up_on_high_miss_rate() {
355 let mut policy = default_policy();
356 policy.cooldown_secs = 0; let mut scaler = PoolScaler::new(policy, 2, 10);
358
359 for _ in 0..4 {
361 scaler.record_acquire(false);
362 }
363 scaler.record_acquire(true);
364
365 assert_eq!(scaler.evaluate(), ScaleDecision::ScaleUp(1));
366 assert_eq!(scaler.current_min_idle(), 3);
367 }
368
369 #[test]
370 fn test_scaler_scale_down_on_low_miss_rate() {
371 let mut policy = default_policy();
372 policy.cooldown_secs = 0;
373 let mut scaler = PoolScaler::new(policy, 3, 10);
374
375 for _ in 0..20 {
377 scaler.record_acquire(true);
378 }
379
380 assert_eq!(scaler.evaluate(), ScaleDecision::ScaleDown(1));
381 assert_eq!(scaler.current_min_idle(), 2);
382 }
383
384 #[test]
385 fn test_scaler_hold_in_normal_range() {
386 let mut policy = default_policy();
387 policy.cooldown_secs = 0;
388 let mut scaler = PoolScaler::new(policy, 3, 10);
389
390 for _ in 0..10 {
392 scaler.record_acquire(true);
393 }
394 scaler.record_acquire(false);
395
396 assert_eq!(scaler.evaluate(), ScaleDecision::Hold);
397 assert_eq!(scaler.current_min_idle(), 3);
398 }
399
400 #[test]
401 fn test_scaler_wont_exceed_max_min_idle() {
402 let mut policy = default_policy();
403 policy.cooldown_secs = 0;
404 policy.max_min_idle = 3;
405 let mut scaler = PoolScaler::new(policy, 3, 10);
406
407 for _ in 0..5 {
409 scaler.record_acquire(false);
410 }
411
412 assert_eq!(scaler.evaluate(), ScaleDecision::Hold);
413 assert_eq!(scaler.current_min_idle(), 3);
414 }
415
416 #[test]
417 fn test_scaler_wont_go_below_one() {
418 let mut policy = default_policy();
419 policy.cooldown_secs = 0;
420 let mut scaler = PoolScaler::new(policy, 1, 10);
421
422 for _ in 0..10 {
424 scaler.record_acquire(true);
425 }
426
427 assert_eq!(scaler.evaluate(), ScaleDecision::Hold);
428 assert_eq!(scaler.current_min_idle(), 1);
429 }
430
431 #[test]
432 fn test_scaler_cooldown_prevents_rapid_scaling() {
433 let policy = default_policy(); let mut scaler = PoolScaler::new(policy, 2, 10);
435
436 for _ in 0..5 {
438 scaler.record_acquire(false);
439 }
440 assert_eq!(scaler.evaluate(), ScaleDecision::ScaleUp(1));
441
442 for _ in 0..5 {
444 scaler.record_acquire(false);
445 }
446 assert_eq!(scaler.evaluate(), ScaleDecision::Hold);
447 }
448
449 #[test]
450 fn test_scaler_successive_scale_ups() {
451 let mut policy = default_policy();
452 policy.cooldown_secs = 0;
453 let mut scaler = PoolScaler::new(policy, 1, 5);
454
455 for _ in 0..3 {
457 for _ in 0..5 {
458 scaler.record_acquire(false);
459 }
460 let decision = scaler.evaluate();
461 assert_eq!(decision, ScaleDecision::ScaleUp(1));
462 }
463 assert_eq!(scaler.current_min_idle(), 4);
464 }
465
466 #[test]
467 fn test_scaler_no_events_holds() {
468 let mut policy = default_policy();
469 policy.cooldown_secs = 0;
470 let mut scaler = PoolScaler::new(policy, 2, 10);
471 assert_eq!(scaler.evaluate(), ScaleDecision::Hold);
472 }
473
474 #[test]
477 fn test_scaling_policy_default() {
478 let policy = ScalingPolicy::default();
479 assert!(!policy.enabled);
480 assert!((policy.scale_up_threshold - 0.3).abs() < 0.001);
481 assert!((policy.scale_down_threshold - 0.05).abs() < 0.001);
482 assert_eq!(policy.max_min_idle, 0);
483 assert_eq!(policy.cooldown_secs, 60);
484 assert_eq!(policy.window_secs, 120);
485 }
486
487 #[test]
488 fn test_scaling_policy_serde_roundtrip() {
489 let policy = ScalingPolicy {
490 enabled: true,
491 scale_up_threshold: 0.4,
492 scale_down_threshold: 0.1,
493 max_min_idle: 8,
494 cooldown_secs: 30,
495 window_secs: 60,
496 };
497 let json = serde_json::to_string(&policy).unwrap();
498 let parsed: ScalingPolicy = serde_json::from_str(&json).unwrap();
499 assert!(parsed.enabled);
500 assert!((parsed.scale_up_threshold - 0.4).abs() < 0.001);
501 assert!((parsed.scale_down_threshold - 0.1).abs() < 0.001);
502 assert_eq!(parsed.max_min_idle, 8);
503 assert_eq!(parsed.cooldown_secs, 30);
504 assert_eq!(parsed.window_secs, 60);
505 }
506
507 #[test]
508 fn test_scaling_policy_deserialize_with_defaults() {
509 let json = r#"{"enabled": true}"#;
510 let policy: ScalingPolicy = serde_json::from_str(json).unwrap();
511 assert!(policy.enabled);
512 assert!((policy.scale_up_threshold - 0.3).abs() < 0.001);
513 assert_eq!(policy.cooldown_secs, 60);
514 assert_eq!(policy.window_secs, 120);
515 }
516
517 #[test]
520 fn test_gateway_pressure_default_zero() {
521 let scaler = PoolScaler::new(default_policy(), 2, 10);
522 assert_eq!(scaler.gateway_pressure(), 0.0);
523 }
524
525 #[test]
526 fn test_update_gateway_pressure() {
527 let mut scaler = PoolScaler::new(default_policy(), 2, 10);
528 scaler.update_gateway_pressure(0.75);
529 assert!((scaler.gateway_pressure() - 0.75).abs() < 0.001);
530 }
531
532 #[test]
533 fn test_gateway_pressure_clamped() {
534 let mut scaler = PoolScaler::new(default_policy(), 2, 10);
535 scaler.update_gateway_pressure(1.5);
536 assert!((scaler.gateway_pressure() - 1.0).abs() < 0.001);
537
538 scaler.update_gateway_pressure(-0.5);
539 assert!((scaler.gateway_pressure() - 0.0).abs() < 0.001);
540 }
541
542 #[test]
543 fn test_gateway_pressure_boosts_scale_up() {
544 let mut policy = default_policy();
545 policy.cooldown_secs = 0;
546 policy.scale_up_threshold = 0.3;
547 let mut scaler = PoolScaler::new(policy, 2, 10);
548
549 scaler.record_acquire(false);
551 for _ in 0..9 {
552 scaler.record_acquire(true);
553 }
554
555 assert_eq!(scaler.evaluate(), ScaleDecision::Hold);
557
558 scaler.update_gateway_pressure(0.8);
560 assert_eq!(scaler.evaluate(), ScaleDecision::ScaleUp(1));
561 }
562
563 #[test]
564 fn test_gateway_pressure_zero_no_effect() {
565 let mut policy = default_policy();
566 policy.cooldown_secs = 0;
567 let mut scaler = PoolScaler::new(policy, 3, 10);
568
569 for _ in 0..10 {
571 scaler.record_acquire(true);
572 }
573 scaler.update_gateway_pressure(0.0);
574
575 assert_eq!(scaler.evaluate(), ScaleDecision::ScaleDown(1));
577 }
578
579 #[test]
580 fn test_gateway_pressure_stale_ignored() {
581 let mut policy = default_policy();
582 policy.cooldown_secs = 0;
583 policy.window_secs = 1; let mut scaler = PoolScaler::new(policy, 2, 10);
585
586 scaler.gateway_pressure = 0.9;
588 scaler.gateway_pressure_at = Some(Instant::now() - std::time::Duration::from_secs(10));
589
590 scaler.record_acquire(false);
592 for _ in 0..9 {
593 scaler.record_acquire(true);
594 }
595
596 assert_eq!(scaler.evaluate(), ScaleDecision::Hold);
598 }
599
600 #[test]
601 fn test_effective_miss_rate_blending() {
602 let mut policy = default_policy();
603 policy.cooldown_secs = 0;
604 let mut scaler = PoolScaler::new(policy, 2, 10);
605
606 for _ in 0..5 {
608 scaler.record_acquire(false);
609 }
610 for _ in 0..5 {
611 scaler.record_acquire(true);
612 }
613
614 let rate = scaler.effective_miss_rate().unwrap();
616 assert!((rate - 0.5).abs() < 0.001);
617
618 scaler.update_gateway_pressure(0.2);
620 let rate = scaler.effective_miss_rate().unwrap();
621 assert!((rate - 0.38).abs() < 0.001);
622 }
623
624 #[test]
625 fn test_gateway_pressure_prevents_scale_down() {
626 let mut policy = default_policy();
627 policy.cooldown_secs = 0;
628 policy.scale_down_threshold = 0.05;
629 let mut scaler = PoolScaler::new(policy, 3, 10);
630
631 for _ in 0..10 {
633 scaler.record_acquire(true);
634 }
635
636 scaler.update_gateway_pressure(0.3);
638 assert_eq!(scaler.evaluate(), ScaleDecision::Hold);
639 }
640}