auth_framework/deployment/
scaling.rs1use 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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
49pub enum ScalingAction {
50 ScaleUp(u32), ScaleDown(u32), NoAction,
53}
54
55#[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
66pub 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 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 pub fn add_metrics(&mut self, metrics: ScalingMetrics) {
89 self.metrics_history.push(metrics);
90
91 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 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 if avg_metrics.cpu_utilization > self.policy.target_cpu_utilization
122 || avg_metrics.memory_utilization > self.policy.target_memory_utilization
123 {
124 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 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 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 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 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 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 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 }
226 }
227
228 self.current_instances = decision.target_instances;
229 Ok(())
230 }
231
232 async fn scale_up(&self, _amount: u32) -> Result<(), ScalingError> {
234 Ok(())
236 }
237
238 async fn scale_down(&self, _amount: u32) -> Result<(), ScalingError> {
240 Ok(())
242 }
243
244 fn calculate_scale_up_amount(&self, metrics: &ScalingMetrics) -> u32 {
246 if metrics.cpu_utilization > 0.9 || metrics.memory_utilization > 0.9 {
249 2 } else {
251 1
252 }
253 }
254
255 fn calculate_scale_down_amount(&self, _metrics: &ScalingMetrics) -> u32 {
257 1
259 }
260
261 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 pub fn get_current_instances(&self) -> u32 {
309 self.current_instances
310 }
311
312 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), scale_down_cooldown: Duration::from_secs(600), metrics_window: Duration::from_secs(300), }
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 let metrics = ScalingMetrics {
375 cpu_utilization: 0.5, memory_utilization: 0.5, 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 let metrics = ScalingMetrics {
399 cpu_utilization: 0.9, memory_utilization: 0.8, 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