Skip to main content

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_or_default()
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_or_default()
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_or_default()
127                    < self.policy.scale_up_cooldown
128            {
129                return Ok(ScalingDecision {
130                    action: ScalingAction::NoAction,
131                    reason: "Scale up cooldown period not yet elapsed".to_string(),
132                    current_instances: self.current_instances,
133                    target_instances: self.current_instances,
134                    timestamp: now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
135                    metrics: avg_metrics,
136                });
137            }
138
139            // Scale up if not at max capacity
140            if self.current_instances < self.policy.max_instances {
141                let scale_amount = self.calculate_scale_up_amount(&avg_metrics);
142                let target_instances =
143                    (self.current_instances + scale_amount).min(self.policy.max_instances);
144
145                return Ok(ScalingDecision {
146                    action: ScalingAction::ScaleUp(target_instances - self.current_instances),
147                    reason: format!(
148                        "High resource utilization: CPU: {:.1}%, Memory: {:.1}%",
149                        avg_metrics.cpu_utilization * 100.0,
150                        avg_metrics.memory_utilization * 100.0
151                    ),
152                    current_instances: self.current_instances,
153                    target_instances,
154                    timestamp: now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
155                    metrics: avg_metrics,
156                });
157            }
158        }
159
160        // Check if we should scale down
161        if avg_metrics.cpu_utilization < self.policy.target_cpu_utilization * 0.5
162            && avg_metrics.memory_utilization < self.policy.target_memory_utilization * 0.5
163        {
164            // Check cooldown period
165            if let Some(last_scale_down) = self.last_scale_down
166                && now.duration_since(last_scale_down).unwrap_or_default()
167                    < self.policy.scale_down_cooldown
168            {
169                return Ok(ScalingDecision {
170                    action: ScalingAction::NoAction,
171                    reason: "Scale down cooldown period not yet elapsed".to_string(),
172                    current_instances: self.current_instances,
173                    target_instances: self.current_instances,
174                    timestamp: now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
175                    metrics: avg_metrics,
176                });
177            }
178
179            // Scale down if not at min capacity
180            if self.current_instances > self.policy.min_instances {
181                let scale_amount = self.calculate_scale_down_amount(&avg_metrics);
182                let target_instances =
183                    (self.current_instances - scale_amount).max(self.policy.min_instances);
184
185                return Ok(ScalingDecision {
186                    action: ScalingAction::ScaleDown(self.current_instances - target_instances),
187                    reason: format!(
188                        "Low resource utilization: CPU: {:.1}%, Memory: {:.1}%",
189                        avg_metrics.cpu_utilization * 100.0,
190                        avg_metrics.memory_utilization * 100.0
191                    ),
192                    current_instances: self.current_instances,
193                    target_instances,
194                    timestamp: now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
195                    metrics: avg_metrics,
196                });
197            }
198        }
199
200        // No action needed
201        Ok(ScalingDecision {
202            action: ScalingAction::NoAction,
203            reason: "Resource utilization within target range".to_string(),
204            current_instances: self.current_instances,
205            target_instances: self.current_instances,
206            timestamp: now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
207            metrics: avg_metrics,
208        })
209    }
210
211    /// Apply scaling decision
212    pub async fn apply_scaling_decision(
213        &mut self,
214        decision: &ScalingDecision,
215    ) -> Result<(), ScalingError> {
216        match &decision.action {
217            ScalingAction::ScaleUp(amount) => {
218                self.scale_up(*amount).await?;
219                self.last_scale_up = Some(SystemTime::now());
220            }
221            ScalingAction::ScaleDown(amount) => {
222                self.scale_down(*amount).await?;
223                self.last_scale_down = Some(SystemTime::now());
224            }
225            ScalingAction::NoAction => {
226                // No action needed
227            }
228        }
229
230        self.current_instances = decision.target_instances;
231        Ok(())
232    }
233
234    /// Scale up by specified amount
235    async fn scale_up(&self, _amount: u32) -> Result<(), ScalingError> {
236        // Implement actual scaling logic
237        Ok(())
238    }
239
240    /// Scale down by specified amount
241    async fn scale_down(&self, _amount: u32) -> Result<(), ScalingError> {
242        // Implement actual scaling logic
243        Ok(())
244    }
245
246    /// Calculate how much to scale up
247    fn calculate_scale_up_amount(&self, metrics: &ScalingMetrics) -> u32 {
248        // Simple algorithm: scale up by 1 instance at a time
249        // More sophisticated algorithms could consider utilization levels
250        if metrics.cpu_utilization > 0.9 || metrics.memory_utilization > 0.9 {
251            2 // Scale more aggressively under high load
252        } else {
253            1
254        }
255    }
256
257    /// Calculate how much to scale down
258    fn calculate_scale_down_amount(&self, _metrics: &ScalingMetrics) -> u32 {
259        // Conservative scale down: 1 instance at a time
260        1
261    }
262
263    /// Get average metrics over the window
264    fn get_average_metrics(&self) -> Result<ScalingMetrics, ScalingError> {
265        if self.metrics_history.is_empty() {
266            return Err(ScalingError::Metrics("No metrics available".to_string()));
267        }
268
269        let count = self.metrics_history.len() as f64;
270        let sum_cpu = self
271            .metrics_history
272            .iter()
273            .map(|m| m.cpu_utilization)
274            .sum::<f64>();
275        let sum_memory = self
276            .metrics_history
277            .iter()
278            .map(|m| m.memory_utilization)
279            .sum::<f64>();
280        let sum_requests = self
281            .metrics_history
282            .iter()
283            .map(|m| m.request_count)
284            .sum::<u64>();
285        let sum_response_time = self
286            .metrics_history
287            .iter()
288            .map(|m| m.response_time.as_millis() as u64)
289            .sum::<u64>();
290        let sum_error_rate = self
291            .metrics_history
292            .iter()
293            .map(|m| m.error_rate)
294            .sum::<f64>();
295
296        Ok(ScalingMetrics {
297            cpu_utilization: sum_cpu / count,
298            memory_utilization: sum_memory / count,
299            request_count: sum_requests / count as u64,
300            response_time: Duration::from_millis(sum_response_time / count as u64),
301            error_rate: sum_error_rate / count,
302            timestamp: SystemTime::now()
303                .duration_since(UNIX_EPOCH)
304                .unwrap_or_default()
305                .as_secs(),
306        })
307    }
308
309    /// Get current instance count
310    pub fn get_current_instances(&self) -> u32 {
311        self.current_instances
312    }
313
314    /// Get scaling policy
315    pub fn get_policy(&self) -> &ScalingPolicy {
316        &self.policy
317    }
318}
319
320impl Default for ScalingPolicy {
321    fn default() -> Self {
322        Self {
323            name: "default".to_string(),
324            enabled: true,
325            min_instances: 1,
326            max_instances: 10,
327            target_cpu_utilization: 0.7,
328            target_memory_utilization: 0.7,
329            scale_up_cooldown: Duration::from_secs(300), // 5 minutes
330            scale_down_cooldown: Duration::from_secs(600), // 10 minutes
331            metrics_window: Duration::from_secs(300),    // 5 minutes
332        }
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_auto_scaler_creation() {
342        let policy = ScalingPolicy::default();
343        let scaler = AutoScaler::new(policy.clone());
344
345        assert_eq!(scaler.current_instances, policy.min_instances);
346        assert_eq!(scaler.policy.name, "default");
347    }
348
349    #[test]
350    fn test_metrics_addition() {
351        let policy = ScalingPolicy::default();
352        let mut scaler = AutoScaler::new(policy);
353
354        let metrics = ScalingMetrics {
355            cpu_utilization: 0.5,
356            memory_utilization: 0.6,
357            request_count: 100,
358            response_time: Duration::from_millis(50),
359            error_rate: 0.01,
360            timestamp: SystemTime::now()
361                .duration_since(UNIX_EPOCH)
362                .unwrap_or_default()
363                .as_secs(),
364        };
365
366        scaler.add_metrics(metrics);
367        assert_eq!(scaler.metrics_history.len(), 1);
368    }
369
370    #[test]
371    fn test_scaling_decision_no_action() {
372        let policy = ScalingPolicy::default();
373        let mut scaler = AutoScaler::new(policy);
374
375        // Add normal metrics
376        let metrics = ScalingMetrics {
377            cpu_utilization: 0.5,    // Below target
378            memory_utilization: 0.5, // Below target
379            request_count: 100,
380            response_time: Duration::from_millis(50),
381            error_rate: 0.01,
382            timestamp: SystemTime::now()
383                .duration_since(UNIX_EPOCH)
384                .unwrap_or_default()
385                .as_secs(),
386        };
387
388        scaler.add_metrics(metrics);
389
390        let decision = scaler.make_scaling_decision().unwrap();
391        assert!(matches!(decision.action, ScalingAction::NoAction));
392    }
393
394    #[test]
395    fn test_scaling_decision_scale_up() {
396        let policy = ScalingPolicy::default();
397        let mut scaler = AutoScaler::new(policy);
398
399        // Add high utilization metrics
400        let metrics = ScalingMetrics {
401            cpu_utilization: 0.9,    // Above target
402            memory_utilization: 0.8, // Above target
403            request_count: 1000,
404            response_time: Duration::from_millis(200),
405            error_rate: 0.05,
406            timestamp: SystemTime::now()
407                .duration_since(UNIX_EPOCH)
408                .unwrap_or_default()
409                .as_secs(),
410        };
411
412        scaler.add_metrics(metrics);
413
414        let decision = scaler.make_scaling_decision().unwrap();
415        assert!(matches!(decision.action, ScalingAction::ScaleUp(_)));
416    }
417
418    #[tokio::test]
419    async fn test_apply_scaling_decision() {
420        let policy = ScalingPolicy::default();
421        let mut scaler = AutoScaler::new(policy);
422
423        let decision = ScalingDecision {
424            action: ScalingAction::ScaleUp(2),
425            reason: "Test scale up".to_string(),
426            current_instances: 1,
427            target_instances: 3,
428            timestamp: SystemTime::now()
429                .duration_since(UNIX_EPOCH)
430                .unwrap_or_default()
431                .as_secs(),
432            metrics: ScalingMetrics {
433                cpu_utilization: 0.9,
434                memory_utilization: 0.8,
435                request_count: 1000,
436                response_time: Duration::from_millis(200),
437                error_rate: 0.05,
438                timestamp: SystemTime::now()
439                    .duration_since(UNIX_EPOCH)
440                    .unwrap_or_default()
441                    .as_secs(),
442            },
443        };
444
445        let result = scaler.apply_scaling_decision(&decision).await;
446        assert!(result.is_ok());
447        assert_eq!(scaler.current_instances, 3);
448    }
449}