1use std::collections::VecDeque;
4
5use crate::config::AutoscaleConfig;
6use crate::spec::Priority;
7use crate::stats::Ewma;
8use crate::time::Time;
9
10const NEGLIGIBLE: f64 = 0.01;
14
15fn slots(x: f64) -> f64 {
17 if x < NEGLIGIBLE { 0.0 } else { x.ceil() }
18}
19
20#[derive(Clone, Debug, Default)]
22pub struct Signals {
23 pub arrivals: u64,
25 pub dt: f64,
27 pub service_secs: f64,
29 pub backlog: [u64; Priority::COUNT],
31 pub busy: u32,
33 pub capacity: u32,
35 pub reserved: u32,
37 pub min: u32,
39 pub max: u32,
41}
42
43#[derive(Clone, Debug, Default)]
46pub struct AutoscaleState {
47 pub arrival_rate: Ewma,
49 pub target: Option<u32>,
51 pub last_desired: u32,
53 history: VecDeque<(Time, u32)>,
54 last_down: Option<Time>,
55}
56
57impl AutoscaleState {
58 pub fn desired(&mut self, cfg: &AutoscaleConfig, s: &Signals) -> u32 {
60 if s.dt > 0.0 {
61 self.arrival_rate
62 .update(s.arrivals as f64 / s.dt, cfg.ewma_alpha);
63 }
64 let lambda = self.arrival_rate.get().unwrap_or(0.0);
65 let svc = s.service_secs.max(1e-9);
66 let base = slots(lambda * svc / cfg.target_utilization.clamp(0.01, 1.0));
67 let backlog: u64 = s.backlog.iter().sum();
68 let drain = (backlog as f64 * svc / cfg.drain_time.as_secs_f64().max(1e-3)).ceil();
69 let mut slo_need = 0.0f64;
70 let mut ahead = 0u64;
71 for c in 0..Priority::COUNT {
72 ahead += s.backlog[c];
73 if let Some(slo) = cfg.slo[c]
74 && s.backlog[c] > 0
75 {
76 slo_need = slo_need.max((ahead as f64 * svc / slo.as_secs_f64().max(1e-3)).ceil());
77 }
78 }
79 let need = (base + drain).max(slo_need).max(s.busy as f64) + s.reserved as f64;
80 let d = (need.min(u32::MAX as f64) as u32).clamp(s.min, s.max.max(s.min));
81 self.last_desired = d;
82 d
83 }
84
85 pub fn evaluate(&mut self, cfg: &AutoscaleConfig, s: &Signals, now: Time) -> Option<u32> {
87 let desired = self.desired(cfg, s);
88 self.history.push_back((now, desired));
89 while let Some(&(t, _)) = self.history.front() {
90 if now.saturating_sub(t) > cfg.stabilization_window {
91 self.history.pop_front();
92 } else {
93 break;
94 }
95 }
96 let current = self.target.unwrap_or(s.capacity);
97 let next = if desired > current {
98 let step = ((current as f64 * cfg.max_step_up_ratio).ceil() as u32)
99 .max(cfg.max_step_up_abs)
100 .max(1);
101 desired.min(current.saturating_add(step))
102 } else if desired < current {
103 let recommended = self
104 .history
105 .iter()
106 .map(|&(_, d)| d)
107 .max()
108 .unwrap_or(desired);
109 let cooled = self
110 .last_down
111 .is_none_or(|t| now.saturating_sub(t) >= cfg.cooldown);
112 if recommended < current && cooled {
113 self.last_down = Some(now);
114 recommended.max(current.saturating_sub(cfg.max_step_down.max(1)))
115 } else {
116 current
117 }
118 } else {
119 current
120 };
121 let next = next.clamp(s.min, s.max.max(s.min));
122 if Some(next) != self.target && (self.target.is_some() || next != s.capacity) {
123 self.target = Some(next);
124 return Some(next);
125 }
126 self.target = Some(next);
127 None
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use core::time::Duration;
135
136 fn sig(backlog: u64, cap: u32) -> Signals {
137 Signals {
138 arrivals: 0,
139 dt: 1.0,
140 service_secs: 1.0,
141 backlog: [0, 0, backlog, 0],
142 busy: cap,
143 capacity: cap,
144 reserved: 0,
145 min: 1,
146 max: 10,
147 }
148 }
149
150 #[test]
151 fn scales_up_on_backlog_and_down_after_stabilization() {
152 let cfg = AutoscaleConfig {
153 stabilization_window: Duration::from_secs(5),
154 cooldown: Duration::from_secs(2),
155 ..Default::default()
156 };
157 let mut st = AutoscaleState::default();
158 assert_eq!(
160 st.evaluate(&cfg, &sig(300, 1), Time::from_secs_f64(1.0)),
161 Some(5)
162 );
163 assert_eq!(
164 st.evaluate(&cfg, &sig(300, 5), Time::from_secs_f64(2.0)),
165 Some(10)
166 );
167 let idle = Signals {
169 busy: 0,
170 ..sig(0, 10)
171 };
172 assert_eq!(st.evaluate(&cfg, &idle, Time::from_secs_f64(3.0)), None);
173 assert_eq!(st.evaluate(&cfg, &idle, Time::from_secs_f64(6.0)), None);
174 assert_eq!(st.evaluate(&cfg, &idle, Time::from_secs_f64(8.5)), Some(9));
176 assert_eq!(st.evaluate(&cfg, &idle, Time::from_secs_f64(9.0)), None);
177 assert_eq!(st.evaluate(&cfg, &idle, Time::from_secs_f64(10.6)), Some(8));
178 }
179
180 #[test]
181 fn an_idle_pool_returns_to_zero() {
182 let cfg = AutoscaleConfig::default();
183 let mut st = AutoscaleState::default();
184 let busy = Signals {
185 arrivals: 8,
186 busy: 0,
187 min: 0,
188 ..sig(0, 0)
189 };
190 assert!(st.desired(&cfg, &busy) >= 1);
191 let idle = Signals {
194 arrivals: 0,
195 ..busy
196 };
197 let mut d = u32::MAX;
198 for _ in 0..60 {
199 d = st.desired(&cfg, &idle);
200 }
201 assert_eq!(d, 0);
202 let mut st = AutoscaleState::default();
204 let trickle = Signals {
205 arrivals: 1,
206 dt: 50.0,
207 ..idle
208 };
209 assert_eq!(st.desired(&cfg, &trickle), 1);
210 }
211
212 #[test]
213 fn slo_drives_capacity() {
214 let mut cfg = AutoscaleConfig::default();
215 cfg.slo[0] = Some(Duration::from_secs(1));
216 let mut st = AutoscaleState::default();
217 let s = Signals {
218 backlog: [6, 0, 0, 0],
219 busy: 0,
220 capacity: 1,
221 max: 100,
222 ..sig(0, 1)
223 };
224 assert_eq!(st.desired(&cfg, &s), 6);
226 }
227
228 #[test]
229 fn reservations_add_capacity() {
230 let cfg = AutoscaleConfig::default();
231 let mut st = AutoscaleState::default();
232 let s = Signals {
233 reserved: 3,
234 busy: 0,
235 ..sig(0, 1)
236 };
237 assert_eq!(st.desired(&cfg, &s), 3);
238 }
239}