1use std::collections::HashMap;
4
5use a3s_box_core::scale::{
6 InstanceEvent, InstanceHealth, InstanceInfo, InstanceState, ScaleRequest, ScaleResponse,
7};
8use chrono::Utc;
9
10use super::{ServiceHealth, ServiceInstances, TrackedInstance};
11
12pub struct ScaleManager {
14 max_instances: u32,
16 services: HashMap<String, ServiceInstances>,
18 events: Vec<InstanceEvent>,
20 max_events: usize,
22}
23
24impl ScaleManager {
25 pub fn new(max_instances: u32) -> Self {
27 Self {
28 max_instances,
29 services: HashMap::new(),
30 events: Vec::new(),
31 max_events: 1000,
32 }
33 }
34
35 pub fn process_request(&mut self, request: &ScaleRequest) -> ScaleResponse {
41 let service = &request.service;
42 let desired = request.replicas;
43
44 let total_other: u32 = self
46 .services
47 .iter()
48 .filter(|(k, _)| k.as_str() != service)
49 .map(|(_, v)| v.instances.len() as u32)
50 .sum();
51
52 let svc = self
54 .services
55 .entry(service.clone())
56 .or_insert_with(|| ServiceInstances {
57 target_replicas: 0,
58 instances: Vec::new(),
59 });
60
61 let current = svc.instances.len() as u32;
62
63 let available = self.max_instances.saturating_sub(total_other);
65 let target = desired.min(available);
66
67 svc.target_replicas = target;
68
69 let accepted = target == desired;
70 let error = if !accepted {
71 Some(format!(
72 "Capped to {} instances (max {} total, {} used by other services)",
73 target, self.max_instances, total_other
74 ))
75 } else {
76 None
77 };
78
79 let instances: Vec<InstanceInfo> = svc
80 .instances
81 .iter()
82 .map(|inst| InstanceInfo {
83 id: inst.id.clone(),
84 state: inst.state,
85 service: service.clone(),
86 created_at: inst.created_at,
87 ready_at: inst.ready_at,
88 endpoint: inst.endpoint.clone(),
89 health: inst.health.clone(),
90 })
91 .collect();
92
93 ScaleResponse {
94 request_id: request.request_id.clone(),
95 accepted,
96 current_replicas: current,
97 target_replicas: target,
98 instances,
99 error,
100 }
101 }
102
103 pub fn register_instance(&mut self, service: &str, instance_id: &str, endpoint: Option<&str>) {
105 let svc = self
106 .services
107 .entry(service.to_string())
108 .or_insert_with(|| ServiceInstances {
109 target_replicas: 0,
110 instances: Vec::new(),
111 });
112
113 if svc.instances.iter().any(|i| i.id == instance_id) {
115 return;
116 }
117
118 svc.instances.push(TrackedInstance {
119 id: instance_id.to_string(),
120 state: InstanceState::Creating,
121 created_at: Utc::now(),
122 ready_at: None,
123 endpoint: endpoint.map(|s| s.to_string()),
124 health: InstanceHealth::default(),
125 });
126 }
127
128 pub fn update_state(
130 &mut self,
131 service: &str,
132 instance_id: &str,
133 new_state: InstanceState,
134 ) -> Option<InstanceEvent> {
135 let svc = self.services.get_mut(service)?;
136 let inst = svc.instances.iter_mut().find(|i| i.id == instance_id)?;
137
138 let old_state = inst.state;
139 if old_state == new_state {
140 return None;
141 }
142
143 inst.state = new_state;
144 if new_state == InstanceState::Ready && inst.ready_at.is_none() {
145 inst.ready_at = Some(Utc::now());
146 }
147
148 let event = InstanceEvent::transition(instance_id, service, old_state, new_state);
149 self.push_event(event.clone());
150 Some(event)
151 }
152
153 pub fn update_health(&mut self, service: &str, instance_id: &str, health: InstanceHealth) {
155 if let Some(svc) = self.services.get_mut(service) {
156 if let Some(inst) = svc.instances.iter_mut().find(|i| i.id == instance_id) {
157 inst.health = health;
158 }
159 }
160 }
161
162 pub fn update_endpoint(&mut self, service: &str, instance_id: &str, endpoint: &str) {
164 if let Some(svc) = self.services.get_mut(service) {
165 if let Some(inst) = svc.instances.iter_mut().find(|i| i.id == instance_id) {
166 inst.endpoint = Some(endpoint.to_string());
167 }
168 }
169 }
170
171 pub fn deregister_instance(&mut self, service: &str, instance_id: &str) -> bool {
173 if let Some(svc) = self.services.get_mut(service) {
174 let before = svc.instances.len();
175 svc.instances.retain(|i| i.id != instance_id);
176 return svc.instances.len() < before;
177 }
178 false
179 }
180
181 pub fn instances_to_create(&self, service: &str) -> u32 {
183 if let Some(svc) = self.services.get(service) {
184 let active = svc
185 .instances
186 .iter()
187 .filter(|i| !matches!(i.state, InstanceState::Stopped | InstanceState::Failed))
188 .count() as u32;
189 svc.target_replicas.saturating_sub(active)
190 } else {
191 0
192 }
193 }
194
195 pub fn instances_to_stop(&self, service: &str) -> Vec<String> {
198 if let Some(svc) = self.services.get(service) {
199 let active: Vec<&TrackedInstance> = svc
200 .instances
201 .iter()
202 .filter(|i| {
203 !matches!(
204 i.state,
205 InstanceState::Stopped
206 | InstanceState::Failed
207 | InstanceState::Stopping
208 | InstanceState::Draining
209 )
210 })
211 .collect();
212
213 let excess = (active.len() as u32).saturating_sub(svc.target_replicas);
214 if excess == 0 {
215 return Vec::new();
216 }
217
218 let mut candidates: Vec<&TrackedInstance> = active;
220 candidates.sort_by_key(|i| match i.state {
221 InstanceState::Ready => 0, InstanceState::Creating => 1, InstanceState::Booting => 2, InstanceState::Busy => 3, _ => 4,
226 });
227
228 candidates
229 .iter()
230 .take(excess as usize)
231 .map(|i| i.id.clone())
232 .collect()
233 } else {
234 Vec::new()
235 }
236 }
237
238 pub fn ready_instances(&self, service: &str) -> Vec<InstanceInfo> {
240 if let Some(svc) = self.services.get(service) {
241 svc.instances
242 .iter()
243 .filter(|i| i.state == InstanceState::Ready)
244 .map(|i| InstanceInfo {
245 id: i.id.clone(),
246 state: i.state,
247 service: service.to_string(),
248 created_at: i.created_at,
249 ready_at: i.ready_at,
250 endpoint: i.endpoint.clone(),
251 health: i.health.clone(),
252 })
253 .collect()
254 } else {
255 Vec::new()
256 }
257 }
258
259 pub fn total_instances(&self) -> u32 {
261 self.services
262 .values()
263 .map(|s| s.instances.len() as u32)
264 .sum()
265 }
266
267 pub fn service_instance_count(&self, service: &str) -> u32 {
269 self.services
270 .get(service)
271 .map(|s| s.instances.len() as u32)
272 .unwrap_or(0)
273 }
274
275 pub fn services(&self) -> Vec<String> {
277 self.services.keys().cloned().collect()
278 }
279
280 pub fn recent_events(&self, limit: usize) -> &[InstanceEvent] {
282 let start = self.events.len().saturating_sub(limit);
283 &self.events[start..]
284 }
285
286 fn push_event(&mut self, event: InstanceEvent) {
287 self.events.push(event);
288 if self.events.len() > self.max_events {
289 self.events.drain(..self.events.len() - self.max_events);
290 }
291 }
292
293 pub fn service_health(&self, service: &str) -> ServiceHealth {
295 let svc = match self.services.get(service) {
296 Some(s) => s,
297 None => return ServiceHealth::default(),
298 };
299
300 let active: Vec<&TrackedInstance> = svc
301 .instances
302 .iter()
303 .filter(|i| matches!(i.state, InstanceState::Ready | InstanceState::Busy))
304 .collect();
305
306 if active.is_empty() {
307 return ServiceHealth {
308 active_instances: 0,
309 ready_instances: 0,
310 busy_instances: 0,
311 ..Default::default()
312 };
313 }
314
315 let ready_count = active
316 .iter()
317 .filter(|i| i.state == InstanceState::Ready)
318 .count() as u32;
319 let busy_count = active
320 .iter()
321 .filter(|i| i.state == InstanceState::Busy)
322 .count() as u32;
323
324 let mut total_cpu = 0.0f64;
325 let mut total_mem = 0u64;
326 let mut total_inflight = 0u32;
327 let mut cpu_count = 0u32;
328 let mut unhealthy = 0u32;
329
330 for inst in &active {
331 if let Some(cpu) = inst.health.cpu_percent {
332 total_cpu += cpu as f64;
333 cpu_count += 1;
334 }
335 if let Some(mem) = inst.health.memory_bytes {
336 total_mem += mem;
337 }
338 total_inflight += inst.health.inflight_requests;
339 if !inst.health.healthy {
340 unhealthy += 1;
341 }
342 }
343
344 ServiceHealth {
345 active_instances: active.len() as u32,
346 ready_instances: ready_count,
347 busy_instances: busy_count,
348 avg_cpu_percent: if cpu_count > 0 {
349 Some((total_cpu / cpu_count as f64) as f32)
350 } else {
351 None
352 },
353 total_memory_bytes: total_mem,
354 total_inflight_requests: total_inflight,
355 unhealthy_instances: unhealthy,
356 }
357 }
358
359 pub fn start_drain(&mut self, service: &str, instance_id: &str) -> Option<InstanceEvent> {
366 let svc = self.services.get_mut(service)?;
367 let inst = svc.instances.iter_mut().find(|i| i.id == instance_id)?;
368
369 if !matches!(inst.state, InstanceState::Ready | InstanceState::Busy) {
371 return None;
372 }
373
374 let old_state = inst.state;
375 inst.state = InstanceState::Draining;
376
377 let event =
378 InstanceEvent::transition(instance_id, service, old_state, InstanceState::Draining)
379 .with_message("Graceful drain initiated");
380 self.push_event(event.clone());
381 Some(event)
382 }
383
384 pub fn complete_drain(&mut self, service: &str, instance_id: &str) -> Option<InstanceEvent> {
388 let svc = self.services.get_mut(service)?;
389 let inst = svc.instances.iter_mut().find(|i| i.id == instance_id)?;
390
391 if inst.state != InstanceState::Draining {
392 return None;
393 }
394
395 inst.state = InstanceState::Stopping;
396
397 let event = InstanceEvent::transition(
398 instance_id,
399 service,
400 InstanceState::Draining,
401 InstanceState::Stopping,
402 )
403 .with_message("Drain complete, stopping instance");
404 self.push_event(event.clone());
405 Some(event)
406 }
407
408 pub fn is_drain_complete(&self, service: &str, instance_id: &str) -> bool {
410 if let Some(svc) = self.services.get(service) {
411 if let Some(inst) = svc.instances.iter().find(|i| i.id == instance_id) {
412 return inst.state == InstanceState::Draining && inst.health.inflight_requests == 0;
413 }
414 }
415 false
416 }
417
418 pub fn draining_instances(&self, service: &str) -> Vec<String> {
420 if let Some(svc) = self.services.get(service) {
421 svc.instances
422 .iter()
423 .filter(|i| i.state == InstanceState::Draining)
424 .map(|i| i.id.clone())
425 .collect()
426 } else {
427 Vec::new()
428 }
429 }
430}