auth_framework/deployment/
scaling.rs

1// Auto-scaling system for production deployment
2// Dynamic resource scaling based on metrics and load
3
4use serde::{Deserialize, Serialize};
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6use thiserror::Error;
7
8#[derive(Debug, Error)]
9pub enum ScalingError {
10    #[error("Scaling policy error: {0}")]
11    Policy(String),
12    #[error("Resource error: {0}")]
13    Resource(String),
14    #[error("Metric collection error: {0}")]
15    Metrics(String),
16    #[error("Scaling operation error: {0}")]
17    Operation(String),
18    #[error("Configuration error: {0}")]
19    Configuration(String),
20}
21
22/// Scaling policy configuration
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ScalingPolicy {
25    pub name: String,
26    pub enabled: bool,
27    pub min_instances: u32,
28    pub max_instances: u32,
29    pub target_cpu_utilization: f64,
30    pub target_memory_utilization: f64,
31    pub scale_up_cooldown: Duration,
32    pub scale_down_cooldown: Duration,
33    pub metrics_window: Duration,
34}
35
36/// Scaling metrics
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ScalingMetrics {
39    pub cpu_utilization: f64,
40    pub memory_utilization: f64,
41    pub request_count: u64,
42    pub response_time: Duration,
43    pub error_rate: f64,
44    pub timestamp: u64,
45}
46
47/// Scaling action
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub enum ScalingAction {
50    ScaleUp(u32),   // Scale up by N instances
51    ScaleDown(u32), // Scale down by N instances
52    NoAction,
53}
54
55/// Scaling decision
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ScalingDecision {
58    pub action: ScalingAction,
59    pub reason: String,
60    pub current_instances: u32,
61    pub target_instances: u32,
62    pub timestamp: u64,
63    pub metrics: ScalingMetrics,
64}
65
66/// Auto-scaling manager
67pub struct AutoScaler {
68    policy: ScalingPolicy,
69    current_instances: u32,
70    last_scale_up: Option<SystemTime>,
71    last_scale_down: Option<SystemTime>,
72    metrics_history: Vec<ScalingMetrics>,
73}
74
75impl AutoScaler {
76    /// Create new auto-scaler
77    pub fn new(policy: ScalingPolicy) -> Self {
78        Self {
79            current_instances: policy.min_instances,
80            policy,
81            last_scale_up: None,
82            last_scale_down: None,
83            metrics_history: Vec::new(),
84        }
85    }
86
87    /// Add metrics sample
88    pub fn add_metrics(&mut self, metrics: ScalingMetrics) {
89        self.metrics_history.push(metrics);
90
91        // Keep only recent metrics within the window
92        let cutoff = SystemTime::now()
93            .duration_since(UNIX_EPOCH)
94            .unwrap()
95            .as_secs()
96            - self.policy.metrics_window.as_secs();
97
98        self.metrics_history.retain(|m| m.timestamp > cutoff);
99    }
100
101    /// Make scaling decision based on current metrics
102    pub fn make_scaling_decision(&self) -> Result<ScalingDecision, ScalingError> {
103        if !self.policy.enabled {
104            return Ok(ScalingDecision {
105                action: ScalingAction::NoAction,
106                reason: "Auto-scaling is disabled".to_string(),
107                current_instances: self.current_instances,
108                target_instances: self.current_instances,
109                timestamp: SystemTime::now()
110                    .duration_since(UNIX_EPOCH)
111                    .unwrap()
112                    .as_secs(),
113                metrics: self.get_average_metrics()?,
114            });
115        }
116
117        let avg_metrics = self.get_average_metrics()?;
118        let now = SystemTime::now();
119
120        // Check if we should scale up
121        if avg_metrics.cpu_utilization > self.policy.target_cpu_utilization
122            || avg_metrics.memory_utilization > self.policy.target_memory_utilization
123        {
124            // Check cooldown period
125            if let Some(last_scale_up) = self.last_scale_up
126                && now.duration_since(last_scale_up).unwrap() < self.policy.scale_up_cooldown
127            {
128                return Ok(ScalingDecision {
129                    action: ScalingAction::NoAction,
130                    reason: "Scale up cooldown period not yet elapsed".to_string(),
131                    current_instances: self.current_instances,
132                    target_instances: self.current_instances,
133                    timestamp: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
134                    metrics: avg_metrics,
135                });
136            }
137
138            // Scale up if not at max capacity
139            if self.current_instances < self.policy.max_instances {
140                let scale_amount = self.calculate_scale_up_amount(&avg_metrics);
141                let target_instances =
142                    (self.current_instances + scale_amount).min(self.policy.max_instances);
143
144                return Ok(ScalingDecision {
145                    action: ScalingAction::ScaleUp(target_instances - self.current_instances),
146                    reason: format!(
147                        "High resource utilization: CPU: {:.1}%, Memory: {:.1}%",
148                        avg_metrics.cpu_utilization * 100.0,
149                        avg_metrics.memory_utilization * 100.0
150                    ),
151                    current_instances: self.current_instances,
152                    target_instances,
153                    timestamp: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
154                    metrics: avg_metrics,
155                });
156            }
157        }
158
159        // Check if we should scale down
160        if avg_metrics.cpu_utilization < self.policy.target_cpu_utilization * 0.5
161            && avg_metrics.memory_utilization < self.policy.target_memory_utilization * 0.5
162        {
163            // Check cooldown period
164            if let Some(last_scale_down) = self.last_scale_down
165                && now.duration_since(last_scale_down).unwrap() < self.policy.scale_down_cooldown
166            {
167                return Ok(ScalingDecision {
168                    action: ScalingAction::NoAction,
169                    reason: "Scale down cooldown period not yet elapsed".to_string(),
170                    current_instances: self.current_instances,
171                    target_instances: self.current_instances,
172                    timestamp: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
173                    metrics: avg_metrics,
174                });
175            }
176
177            // Scale down if not at min capacity
178            if self.current_instances > self.policy.min_instances {
179                let scale_amount = self.calculate_scale_down_amount(&avg_metrics);
180                let target_instances =
181                    (self.current_instances - scale_amount).max(self.policy.min_instances);
182
183                return Ok(ScalingDecision {
184                    action: ScalingAction::ScaleDown(self.current_instances - target_instances),
185                    reason: format!(
186                        "Low resource utilization: CPU: {:.1}%, Memory: {:.1}%",
187                        avg_metrics.cpu_utilization * 100.0,
188                        avg_metrics.memory_utilization * 100.0
189                    ),
190                    current_instances: self.current_instances,
191                    target_instances,
192                    timestamp: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
193                    metrics: avg_metrics,
194                });
195            }
196        }
197
198        // No action needed
199        Ok(ScalingDecision {
200            action: ScalingAction::NoAction,
201            reason: "Resource utilization within target range".to_string(),
202            current_instances: self.current_instances,
203            target_instances: self.current_instances,
204            timestamp: now.duration_since(UNIX_EPOCH).unwrap().as_secs(),
205            metrics: avg_metrics,
206        })
207    }
208
209    /// Apply scaling decision
210    pub async fn apply_scaling_decision(
211        &mut self,
212        decision: &ScalingDecision,
213    ) -> Result<(), ScalingError> {
214        match &decision.action {
215            ScalingAction::ScaleUp(amount) => {
216                self.scale_up(*amount).await?;
217                self.last_scale_up = Some(SystemTime::now());
218            }
219            ScalingAction::ScaleDown(amount) => {
220                self.scale_down(*amount).await?;
221                self.last_scale_down = Some(SystemTime::now());
222            }
223            ScalingAction::NoAction => {
224                // No action needed
225            }
226        }
227
228        self.current_instances = decision.target_instances;
229        Ok(())
230    }
231
232    /// Scale up by specified amount
233    async fn scale_up(&self, _amount: u32) -> Result<(), ScalingError> {
234        // Implement actual scaling logic
235        Ok(())
236    }
237
238    /// Scale down by specified amount
239    async fn scale_down(&self, _amount: u32) -> Result<(), ScalingError> {
240        // Implement actual scaling logic
241        Ok(())
242    }
243
244    /// Calculate how much to scale up
245    fn calculate_scale_up_amount(&self, metrics: &ScalingMetrics) -> u32 {
246        // Simple algorithm: scale up by 1 instance at a time
247        // More sophisticated algorithms could consider utilization levels
248        if metrics.cpu_utilization > 0.9 || metrics.memory_utilization > 0.9 {
249            2 // Scale more aggressively under high load
250        } else {
251            1
252        }
253    }
254
255    /// Calculate how much to scale down
256    fn calculate_scale_down_amount(&self, _metrics: &ScalingMetrics) -> u32 {
257        // Conservative scale down: 1 instance at a time
258        1
259    }
260
261    /// Get average metrics over the window
262    fn get_average_metrics(&self) -> Result<ScalingMetrics, ScalingError> {
263        if self.metrics_history.is_empty() {
264            return Err(ScalingError::Metrics("No metrics available".to_string()));
265        }
266
267        let count = self.metrics_history.len() as f64;
268        let sum_cpu = self
269            .metrics_history
270            .iter()
271            .map(|m| m.cpu_utilization)
272            .sum::<f64>();
273        let sum_memory = self
274            .metrics_history
275            .iter()
276            .map(|m| m.memory_utilization)
277            .sum::<f64>();
278        let sum_requests = self
279            .metrics_history
280            .iter()
281            .map(|m| m.request_count)
282            .sum::<u64>();
283        let sum_response_time = self
284            .metrics_history
285            .iter()
286            .map(|m| m.response_time.as_millis() as u64)
287            .sum::<u64>();
288        let sum_error_rate = self
289            .metrics_history
290            .iter()
291            .map(|m| m.error_rate)
292            .sum::<f64>();
293
294        Ok(ScalingMetrics {
295            cpu_utilization: sum_cpu / count,
296            memory_utilization: sum_memory / count,
297            request_count: sum_requests / count as u64,
298            response_time: Duration::from_millis(sum_response_time / count as u64),
299            error_rate: sum_error_rate / count,
300            timestamp: SystemTime::now()
301                .duration_since(UNIX_EPOCH)
302                .unwrap()
303                .as_secs(),
304        })
305    }
306
307    /// Get current instance count
308    pub fn get_current_instances(&self) -> u32 {
309        self.current_instances
310    }
311
312    /// Get scaling policy
313    pub fn get_policy(&self) -> &ScalingPolicy {
314        &self.policy
315    }
316}
317
318impl Default for ScalingPolicy {
319    fn default() -> Self {
320        Self {
321            name: "default".to_string(),
322            enabled: true,
323            min_instances: 1,
324            max_instances: 10,
325            target_cpu_utilization: 0.7,
326            target_memory_utilization: 0.7,
327            scale_up_cooldown: Duration::from_secs(300), // 5 minutes
328            scale_down_cooldown: Duration::from_secs(600), // 10 minutes
329            metrics_window: Duration::from_secs(300),    // 5 minutes
330        }
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn test_auto_scaler_creation() {
340        let policy = ScalingPolicy::default();
341        let scaler = AutoScaler::new(policy.clone());
342
343        assert_eq!(scaler.current_instances, policy.min_instances);
344        assert_eq!(scaler.policy.name, "default");
345    }
346
347    #[test]
348    fn test_metrics_addition() {
349        let policy = ScalingPolicy::default();
350        let mut scaler = AutoScaler::new(policy);
351
352        let metrics = ScalingMetrics {
353            cpu_utilization: 0.5,
354            memory_utilization: 0.6,
355            request_count: 100,
356            response_time: Duration::from_millis(50),
357            error_rate: 0.01,
358            timestamp: SystemTime::now()
359                .duration_since(UNIX_EPOCH)
360                .unwrap()
361                .as_secs(),
362        };
363
364        scaler.add_metrics(metrics);
365        assert_eq!(scaler.metrics_history.len(), 1);
366    }
367
368    #[test]
369    fn test_scaling_decision_no_action() {
370        let policy = ScalingPolicy::default();
371        let mut scaler = AutoScaler::new(policy);
372
373        // Add normal metrics
374        let metrics = ScalingMetrics {
375            cpu_utilization: 0.5,    // Below target
376            memory_utilization: 0.5, // Below target
377            request_count: 100,
378            response_time: Duration::from_millis(50),
379            error_rate: 0.01,
380            timestamp: SystemTime::now()
381                .duration_since(UNIX_EPOCH)
382                .unwrap()
383                .as_secs(),
384        };
385
386        scaler.add_metrics(metrics);
387
388        let decision = scaler.make_scaling_decision().unwrap();
389        assert!(matches!(decision.action, ScalingAction::NoAction));
390    }
391
392    #[test]
393    fn test_scaling_decision_scale_up() {
394        let policy = ScalingPolicy::default();
395        let mut scaler = AutoScaler::new(policy);
396
397        // Add high utilization metrics
398        let metrics = ScalingMetrics {
399            cpu_utilization: 0.9,    // Above target
400            memory_utilization: 0.8, // Above target
401            request_count: 1000,
402            response_time: Duration::from_millis(200),
403            error_rate: 0.05,
404            timestamp: SystemTime::now()
405                .duration_since(UNIX_EPOCH)
406                .unwrap()
407                .as_secs(),
408        };
409
410        scaler.add_metrics(metrics);
411
412        let decision = scaler.make_scaling_decision().unwrap();
413        assert!(matches!(decision.action, ScalingAction::ScaleUp(_)));
414    }
415
416    #[tokio::test]
417    async fn test_apply_scaling_decision() {
418        let policy = ScalingPolicy::default();
419        let mut scaler = AutoScaler::new(policy);
420
421        let decision = ScalingDecision {
422            action: ScalingAction::ScaleUp(2),
423            reason: "Test scale up".to_string(),
424            current_instances: 1,
425            target_instances: 3,
426            timestamp: SystemTime::now()
427                .duration_since(UNIX_EPOCH)
428                .unwrap()
429                .as_secs(),
430            metrics: ScalingMetrics {
431                cpu_utilization: 0.9,
432                memory_utilization: 0.8,
433                request_count: 1000,
434                response_time: Duration::from_millis(200),
435                error_rate: 0.05,
436                timestamp: SystemTime::now()
437                    .duration_since(UNIX_EPOCH)
438                    .unwrap()
439                    .as_secs(),
440            },
441        };
442
443        let result = scaler.apply_scaling_decision(&decision).await;
444        assert!(result.is_ok());
445        assert_eq!(scaler.current_instances, 3);
446    }
447}
448
449