//! Load balancer and auto-scaling implementation use crate::error::{ApiError, ApiResult}; use crate::grpc::ScalingAction; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; /// Load balancing strategy #[derive(Debug, Clone, Copy, PartialEq)] pub enum BalancingStrategy { RoundRobin, LeastConnections, WeightedRoundRobin, Random, IpHash, } /// Load balancer configuration #[derive(Debug, Clone)] pub struct LoadBalancerConfig { pub strategy: BalancingStrategy, pub health_check_interval: Option, pub connection_timeout: Duration, pub request_timeout: Duration, } impl Default for LoadBalancerConfig { fn default() -> Self { Self { strategy: BalancingStrategy::RoundRobin, health_check_interval: Some(Duration::from_secs(30)), connection_timeout: Duration::from_secs(5), request_timeout: Duration::from_secs(30), } } } impl LoadBalancerConfig { #[must_use] pub fn with_strategy(strategy: BalancingStrategy) -> Self { Self { strategy, ..Default::default() } } pub fn enable_health_checks(&mut self, interval: Duration) { self.health_check_interval = Some(interval); } } /// Backend server health status #[derive(Debug, Clone, Copy, PartialEq)] pub enum BackendHealth { Healthy, Unhealthy, Unknown, } /// Backend server representation #[derive(Debug, Clone)] pub struct Backend { name: String, url: String, weight: usize, active_connections: usize, health: BackendHealth, last_health_check: Option, } impl Backend { #[must_use] pub fn new(name: &str, url: &str) -> Self { Self { name: name.to_string(), url: url.to_string(), weight: 1, active_connections: 0, health: BackendHealth::Unknown, last_health_check: None, } } #[must_use] pub fn name(&self) -> &str { &self.name } pub fn set_weight(&mut self, weight: usize) { self.weight = weight; } pub fn set_active_connections(&mut self, count: usize) { self.active_connections = count; } pub fn mark_unhealthy(&mut self) { self.health = BackendHealth::Unhealthy; } #[must_use] pub fn is_healthy(&self) -> bool { self.health == BackendHealth::Healthy } } /// Load balancer for distributing requests #[derive(Debug)] pub struct LoadBalancer { config: LoadBalancerConfig, backends: Arc>>, current_index: Arc>, } impl LoadBalancer { #[must_use] pub fn new(config: LoadBalancerConfig) -> Self { Self { config, backends: Arc::new(RwLock::new(Vec::new())), current_index: Arc::new(RwLock::new(0)), } } #[must_use] pub fn strategy(&self) -> BalancingStrategy { self.config.strategy } #[must_use] pub fn num_backends(&self) -> usize { futures::executor::block_on(async { self.backends.read().await.len() }) } #[must_use] pub fn is_ready(&self) -> bool { self.num_backends() > 0 } pub async fn add_backend(&mut self, backend: Backend) -> ApiResult<()> { let mut backends = self.backends.write().await; backends.push(backend); Ok(()) } #[must_use] pub fn has_backend(&self, name: &str) -> bool { futures::executor::block_on(async { self.backends.read().await.iter().any(|b| b.name == name) }) } #[must_use] pub fn has_healthy_backend(&self, name: &str) -> bool { futures::executor::block_on(async { self.backends .read() .await .iter() .any(|b| b.name == name && b.is_healthy()) }) } pub async fn next_backend(&self) -> ApiResult { let backends = self.backends.read().await; if backends.is_empty() { return Err(ApiError::LoadBalancer("No backends available".to_string())); } match self.config.strategy { BalancingStrategy::RoundRobin => { let mut index = self.current_index.write().await; let backend = backends[*index % backends.len()].clone(); *index += 1; Ok(backend) } BalancingStrategy::LeastConnections => { let backend = backends .iter() .filter(|b| b.is_healthy()) .min_by_key(|b| b.active_connections) .ok_or_else(|| ApiError::LoadBalancer("No healthy backends".to_string()))? .clone(); Ok(backend) } BalancingStrategy::WeightedRoundRobin => { let mut index = self.current_index.write().await; // Build weighted list let mut weighted_backends = Vec::new(); for backend in backends.iter() { for _ in 0..backend.weight { weighted_backends.push(backend.clone()); } } if weighted_backends.is_empty() { return Err(ApiError::LoadBalancer("No weighted backends".to_string())); } let backend = weighted_backends[*index % weighted_backends.len()].clone(); *index += 1; Ok(backend) } _ => { // Default to round-robin for other strategies let mut index = self.current_index.write().await; let backend = backends[*index % backends.len()].clone(); *index += 1; Ok(backend) } } } pub async fn check_backend_health(&self, name: &str) -> ApiResult { let backends = self.backends.read().await; let backend = backends .iter() .find(|b| b.name == name) .ok_or_else(|| ApiError::LoadBalancer(format!("Backend {name} not found")))?; // Simulate health check Ok(backend.health) } pub async fn perform_health_checks(&mut self) { let mut backends = self.backends.write().await; for backend in backends.iter_mut() { // Simulate health check if backend.health == BackendHealth::Unhealthy { // Keep unhealthy backends marked as such backend.last_health_check = Some(Instant::now()); } else { backend.health = BackendHealth::Healthy; backend.last_health_check = Some(Instant::now()); } } // Remove persistently unhealthy backends backends.retain(|b| b.health != BackendHealth::Unhealthy); } } /// Auto-scaling policy #[derive(Debug, Clone)] pub enum ScalingPolicy { CpuBased { target: f64 }, RequestRateBased { target_rps: f64 }, Predictive, Custom(String), } /// Auto-scaler configuration #[derive(Debug, Clone)] pub struct AutoScalerConfig { pub min_instances: usize, pub max_instances: usize, pub scaling_policy: ScalingPolicy, pub cooldown_period: Duration, pub scale_up_threshold: f64, pub scale_down_threshold: f64, } impl Default for AutoScalerConfig { fn default() -> Self { Self { min_instances: 1, max_instances: 10, scaling_policy: ScalingPolicy::CpuBased { target: 70.0 }, cooldown_period: Duration::from_secs(300), scale_up_threshold: 0.8, scale_down_threshold: 0.3, } } } impl AutoScalerConfig { pub fn set_min_instances(&mut self, min: usize) { self.min_instances = min; } pub fn set_max_instances(&mut self, max: usize) { self.max_instances = max; } pub fn set_scaling_policy(&mut self, policy: ScalingPolicy) { self.scaling_policy = policy; } pub fn set_cooldown_period(&mut self, period: Duration) { self.cooldown_period = period; } } /// Metrics collector for auto-scaling decisions #[derive(Debug)] pub struct MetricsCollector { cpu_usage: Arc>>, request_rate: Arc>>, memory_usage: Arc>>, } impl Default for MetricsCollector { fn default() -> Self { Self::new() } } impl MetricsCollector { #[must_use] pub fn new() -> Self { Self { cpu_usage: Arc::new(RwLock::new(Vec::new())), request_rate: Arc::new(RwLock::new(Vec::new())), memory_usage: Arc::new(RwLock::new(Vec::new())), } } pub async fn record_cpu_usage(&self, usage: f64) { let mut cpu = self.cpu_usage.write().await; cpu.push(usage); // Keep only last 100 samples if cpu.len() > 100 { cpu.remove(0); } } pub async fn record_request_rate(&self, rate: f64) { let mut rps = self.request_rate.write().await; rps.push(rate); if rps.len() > 100 { rps.remove(0); } } pub async fn get_average_cpu(&self) -> f64 { let cpu = self.cpu_usage.read().await; if cpu.is_empty() { return 0.0; } cpu.iter().sum::() / cpu.len() as f64 } pub async fn get_average_request_rate(&self) -> f64 { let rps = self.request_rate.read().await; if rps.is_empty() { return 0.0; } rps.iter().sum::() / rps.len() as f64 } } /// Auto-scaler for dynamic instance management #[derive(Debug)] pub struct AutoScaler { config: AutoScalerConfig, last_scaling_time: Arc>>, } impl AutoScaler { #[must_use] pub fn new(config: AutoScalerConfig) -> Self { Self { config, last_scaling_time: Arc::new(RwLock::new(None)), } } #[must_use] pub fn min_instances(&self) -> usize { self.config.min_instances } #[must_use] pub fn max_instances(&self) -> usize { self.config.max_instances } #[must_use] pub fn target_cpu_utilization(&self) -> f64 { match &self.config.scaling_policy { ScalingPolicy::CpuBased { target } => *target, _ => 70.0, } } pub async fn evaluate( &mut self, lb: &LoadBalancer, metrics: &MetricsCollector, ) -> ApiResult { // Check cooldown period if !self.can_scale().await { return Ok(ScalingAction::None); } let current_instances = lb.num_backends(); match &self.config.scaling_policy { ScalingPolicy::CpuBased { target } => { let avg_cpu = metrics.get_average_cpu().await; if avg_cpu > target * self.config.scale_up_threshold { if current_instances < self.config.max_instances { return Ok(ScalingAction::ScaleUp(1)); } } else if avg_cpu < target * self.config.scale_down_threshold && current_instances > self.config.min_instances { return Ok(ScalingAction::ScaleDown(1)); } } ScalingPolicy::RequestRateBased { target_rps } => { let avg_rps = metrics.get_average_request_rate().await; let rps_per_instance = avg_rps / current_instances as f64; if rps_per_instance > *target_rps { if current_instances < self.config.max_instances { return Ok(ScalingAction::ScaleUp(1)); } } else if rps_per_instance < target_rps * 0.5 && current_instances > self.config.min_instances { return Ok(ScalingAction::ScaleDown(1)); } } ScalingPolicy::Predictive => { // Simplified predictive scaling let cpu_history = metrics.cpu_usage.read().await; if cpu_history.len() >= 5 { let recent = &cpu_history[cpu_history.len() - 5..]; let trend: f64 = recent.windows(2).map(|w| w[1] - w[0]).sum::() / 4.0; if trend > 5.0 && current_instances < self.config.max_instances { return Ok(ScalingAction::ScaleUp(1)); } } } _ => {} } Ok(ScalingAction::None) } pub async fn apply_scaling(&mut self, action: ScalingAction) -> ApiResult<()> { if action != ScalingAction::None { let mut last_time = self.last_scaling_time.write().await; *last_time = Some(Instant::now()); } Ok(()) } async fn can_scale(&self) -> bool { let last_time = self.last_scaling_time.read().await; match *last_time { Some(time) => time.elapsed() >= self.config.cooldown_period, None => true, } } }