Skip to main content

a3s_box_runtime/scale/
manager.rs

1//! ScaleManager implementation.
2
3use 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
12/// Tracks all instances managed by this Box host.
13pub struct ScaleManager {
14    /// Maximum total instances across all services
15    max_instances: u32,
16    /// Per-service instance tracking
17    services: HashMap<String, ServiceInstances>,
18    /// Event log (bounded ring buffer)
19    events: Vec<InstanceEvent>,
20    /// Maximum events to retain
21    max_events: usize,
22}
23
24impl ScaleManager {
25    /// Create a new scale manager with the given capacity.
26    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    /// Process a scale request and return the response.
36    ///
37    /// This determines how many instances to create or destroy
38    /// but does not actually start/stop VMs — the caller is responsible
39    /// for that based on the response.
40    pub fn process_request(&mut self, request: &ScaleRequest) -> ScaleResponse {
41        let service = &request.service;
42        let desired = request.replicas;
43
44        // Compute total instances in other services first (before mutable borrow)
45        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        // Get or create service entry
53        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        // Check capacity
64        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    /// Register a new instance for a service.
104    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        // Don't register duplicates
114        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    /// Update an instance's state and emit a transition event.
129    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    /// Update an instance's health metrics.
154    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    /// Update an instance's endpoint.
163    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    /// Remove an instance from tracking.
172    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    /// Get instances that need to be created (target > current running).
182    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    /// Get instances that should be stopped (current > target).
196    /// Returns instance IDs to stop, preferring non-busy instances.
197    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            // Prefer stopping idle (Ready) instances over Busy ones
219            let mut candidates: Vec<&TrackedInstance> = active;
220            candidates.sort_by_key(|i| match i.state {
221                InstanceState::Ready => 0,    // Stop idle first
222                InstanceState::Creating => 1, // Then creating
223                InstanceState::Booting => 2,  // Then booting
224                InstanceState::Busy => 3,     // Busy last
225                _ => 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    /// Get all ready instances for a service (for traffic routing).
239    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    /// Get the total number of instances across all services.
260    pub fn total_instances(&self) -> u32 {
261        self.services
262            .values()
263            .map(|s| s.instances.len() as u32)
264            .sum()
265    }
266
267    /// Get the number of instances for a specific service.
268    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    /// List all tracked services.
276    pub fn services(&self) -> Vec<String> {
277        self.services.keys().cloned().collect()
278    }
279
280    /// Get recent events.
281    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    /// Aggregate health metrics for a service (for autoscaler decisions).
294    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    /// Initiate graceful drain for an instance.
360    ///
361    /// Transitions the instance to `Draining` state. The caller should:
362    /// 1. Stop routing new requests to this instance
363    /// 2. Wait for in-flight requests to complete (or timeout)
364    /// 3. Call `complete_drain()` to transition to `Stopping`
365    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        // Can only drain from Ready or Busy
370        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    /// Complete a drain and transition to Stopping.
385    ///
386    /// Called after in-flight requests have completed or the drain timeout expired.
387    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    /// Check if a draining instance has no in-flight requests.
409    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    /// Get all instances currently draining.
419    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}