//! Nightly Chaos Job Automation //! //! Automated scheduling and execution of chaos engineering tests //! for continuous validation of system resilience. use anyhow::{Context, Result}; use chrono::{Datelike, DateTime, NaiveTime, Utc}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use tokio::fs; use tokio::sync::{broadcast, RwLock}; use tokio::time::{interval, sleep_until, timeout, Instant}; use tracing::{error, info, warn}; use uuid::Uuid; use super::chaos_framework::ChaosResult; use super::ml_training_chaos::{MLChaosConfig, MLChaosResult, MLTrainingChaosTests}; /// Nightly chaos job configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NightlyChaosConfig { pub enabled: bool, pub schedule_time: NaiveTime, // Time to run chaos tests (e.g., 2:00 AM) pub timezone: String, // Timezone for scheduling (e.g., "UTC", "America/New_York") pub max_duration_hours: u8, // Maximum time chaos tests can run pub notification_webhook: Option, // Slack/Teams webhook for alerts pub report_storage_path: PathBuf, pub ml_chaos_config: MLChaosConfig, pub exclude_weekends: bool, pub retry_on_failure: bool, pub max_retries: u8, } impl Default for NightlyChaosConfig { fn default() -> Self { Self { enabled: true, schedule_time: NaiveTime::from_hms_opt(2, 0, 0).unwrap(), // 2:00 AM timezone: "UTC".to_string(), max_duration_hours: 4, notification_webhook: None, report_storage_path: PathBuf::from("/tmp/chaos_reports"), ml_chaos_config: MLChaosConfig { ml_service_endpoint: "http://localhost:8080".to_string(), checkpoint_base_path: PathBuf::from("/tmp/ml_checkpoints"), model_types: vec![ super::ml_training_chaos::ModelType::TLOB, super::ml_training_chaos::ModelType::DQN, super::ml_training_chaos::ModelType::MAMBA2, ], training_timeout_secs: 300, max_recovery_time_ms: 100, // HFT requirement gpu_memory_threshold_mb: 8192, }, exclude_weekends: true, retry_on_failure: true, max_retries: 2, } } } /// Chaos job execution status #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ChaosJobStatus { Scheduled, Running, Completed, Failed, Cancelled, Retrying { attempt: u8 }, } /// Chaos job execution record #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChaosJobExecution { pub id: Uuid, pub scheduled_at: DateTime, pub started_at: Option>, pub completed_at: Option>, pub status: ChaosJobStatus, pub chaos_results: Vec, pub ml_chaos_results: Vec, pub total_experiments: usize, pub successful_experiments: usize, pub failed_experiments: usize, pub report_path: Option, pub error_messages: Vec, pub performance_summary: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PerformanceSummary { pub total_recovery_time_ms: u64, pub average_recovery_time_ms: f64, pub max_recovery_time_ms: u64, pub sla_violations: usize, // Recovery times > max_recovery_time_ms pub performance_regressions: usize, pub checkpoint_failures: usize, } /// Nightly chaos job scheduler and runner pub struct NightlyChaosRunner { config: Arc>, job_history: Arc>>, _event_sender: broadcast::Sender, is_running: Arc>, } #[derive(Debug, Clone)] pub enum ChaosJobEvent { JobScheduled { id: Uuid, scheduled_time: DateTime, }, JobStarted { id: Uuid, }, JobCompleted { id: Uuid, summary: PerformanceSummary, }, JobFailed { id: Uuid, error: String, }, JobRetrying { id: Uuid, attempt: u8, }, AlertTriggered { message: String, severity: AlertSeverity, }, } #[derive(Debug, Clone)] pub enum AlertSeverity { Info, Warning, Critical, } impl NightlyChaosRunner { pub fn new(config: NightlyChaosConfig) -> Self { let (_event_sender, _) = broadcast::channel(1000); Self { config: Arc::new(RwLock::new(config)), job_history: Arc::new(RwLock::new(Vec::new())), _event_sender, is_running: Arc::new(RwLock::new(false)), } } /// Start the nightly chaos job scheduler pub async fn start(&self) -> Result<()> { { let mut running = self.is_running.write().await; if *running { return Err(anyhow::anyhow!("Chaos runner is already running")); } *running = true; } info!("Starting nightly chaos job scheduler"); let config = self.config.read().await.clone(); if !config.enabled { warn!("Nightly chaos jobs are disabled in configuration"); return Ok(()); } // Create report storage directory fs::create_dir_all(&config.report_storage_path) .await .context("Failed to create report storage directory")?; // Start scheduling loop let scheduler_handle = { let runner = self.clone(); tokio::spawn(async move { runner.scheduling_loop().await; }) }; // Wait for the scheduler (it runs indefinitely) let _ = scheduler_handle.await; Ok(()) } /// Stop the chaos job scheduler pub async fn stop(&self) { let mut running = self.is_running.write().await; *running = false; info!("Stopped nightly chaos job scheduler"); } /// Main scheduling loop async fn scheduling_loop(&self) { let mut check_interval = interval(Duration::from_secs(60)); // Check every minute loop { check_interval.tick().await; // Check if we should stop { let running = self.is_running.read().await; if !*running { break; } } let config = self.config.read().await.clone(); if !config.enabled { continue; } // Check if it's time to run chaos tests if self.should_run_chaos_tests(&config).await { match self.schedule_chaos_job().await { Ok(job_id) => { info!("Scheduled chaos job: {}", job_id); // Execute the job let runner = self.clone(); tokio::spawn(async move { if let Err(e) = runner.execute_chaos_job(job_id).await { error!("Chaos job execution failed: {}", e); } }); } Err(e) => { error!("Failed to schedule chaos job: {}", e); } } // Wait 24 hours before next check to avoid duplicate runs sleep_until(Instant::now() + Duration::from_secs(24 * 60 * 60)).await; } } } /// Check if chaos tests should run now async fn should_run_chaos_tests(&self, config: &NightlyChaosConfig) -> bool { let now = Utc::now(); // Skip weekends if configured if config.exclude_weekends { let weekday = now.weekday(); if weekday == chrono::Weekday::Sat || weekday == chrono::Weekday::Sun { return false; } } // Check if it's the scheduled time (within 1 minute window) let current_time = now.time(); let schedule_time = config.schedule_time; let diff = if current_time >= schedule_time { current_time - schedule_time } else { // Handle day boundary chrono::NaiveTime::from_hms_opt(23, 59, 59).unwrap() - schedule_time + chrono::Duration::seconds(60) + (current_time - chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap()) }; // Run if within 1 minute of scheduled time diff <= Duration::minutes(1) } /// Schedule a new chaos job async fn schedule_chaos_job(&self) -> Result { let job_id = Uuid::new_v4(); let now = Utc::now(); let job = ChaosJobExecution { id: job_id, scheduled_at: now, started_at: None, completed_at: None, status: ChaosJobStatus::Scheduled, chaos_results: Vec::new(), ml_chaos_results: Vec::new(), total_experiments: 0, successful_experiments: 0, failed_experiments: 0, report_path: None, error_messages: Vec::new(), performance_summary: None, }; // Add to job history { let mut history = self.job_history.write().await; history.push(job.clone()); } // Send scheduling event let _ = self._event_sender.send(ChaosJobEvent::JobScheduled { id: job_id, scheduled_time: now, }); Ok(job_id) } /// Execute a chaos job async fn execute_chaos_job(&self, job_id: Uuid) -> Result<()> { info!("Executing chaos job: {}", job_id); // Update job status self.update_job_status(job_id, ChaosJobStatus::Running) .await?; self.update_job_start_time(job_id, Some(Utc::now())).await?; // Send start event let _ = self ._event_sender .send(ChaosJobEvent::JobStarted { id: job_id }); let config = self.config.read().await.clone(); let mut attempt = 1; loop { match self.run_chaos_experiments(job_id, &config).await { Ok(summary) => { // Job succeeded self.update_job_status(job_id, ChaosJobStatus::Completed) .await?; self.update_job_completion_time(job_id, Some(Utc::now())) .await?; self.update_job_performance_summary(job_id, Some(summary.clone())) .await?; // Generate and save report if let Err(e) = self.generate_and_save_report(job_id).await { error!("Failed to generate chaos job report: {}", e); } // Send completion event let _ = self._event_sender.send(ChaosJobEvent::JobCompleted { id: job_id, summary: summary.clone(), }); // Send alerts if needed self.check_and_send_alerts(job_id, &summary).await; info!("Chaos job completed successfully: {}", job_id); break; } Err(e) => { error!("Chaos job failed (attempt {}): {}", attempt, e); if config.retry_on_failure && attempt <= config.max_retries { // Retry the job self.update_job_status(job_id, ChaosJobStatus::Retrying { attempt }) .await?; self.add_job_error(job_id, format!("Attempt {} failed: {}", attempt, e)) .await?; let _ = self._event_sender.send(ChaosJobEvent::JobRetrying { id: job_id, attempt, }); attempt += 1; sleep_until(Instant::now() + Duration::from_secs(300)).await; // Wait 5 minutes before retry continue; } else { // Job failed permanently self.update_job_status(job_id, ChaosJobStatus::Failed) .await?; self.update_job_completion_time(job_id, Some(Utc::now())) .await?; self.add_job_error(job_id, e.to_string()).await?; let _ = self._event_sender.send(ChaosJobEvent::JobFailed { id: job_id, error: e.to_string(), }); error!("Chaos job failed permanently: {}", job_id); break; } } } } Ok(()) } /// Run all chaos experiments async fn run_chaos_experiments( &self, job_id: Uuid, config: &NightlyChaosConfig, ) -> Result { info!("Running chaos experiments for job: {}", job_id); // Initialize ML chaos tests let ml_chaos_tests = MLTrainingChaosTests::new(config.ml_chaos_config.clone()); // Run ML-specific chaos experiments let ml_results = timeout( Duration::from_secs(config.max_duration_hours as u64 * 3600), ml_chaos_tests.run_ml_chaos_suite(), ) .await .context("ML chaos tests timed out")? .context("ML chaos tests failed")?; // Update job with ML results self.update_job_ml_results(job_id, ml_results.clone()) .await?; // Calculate performance summary let summary = self.calculate_performance_summary(&[], &ml_results); Ok(summary) } /// Calculate performance summary from results fn calculate_performance_summary( &self, chaos_results: &[ChaosResult], ml_results: &[MLChaosResult], ) -> PerformanceSummary { let mut total_recovery_time_ms = 0u64; let mut recovery_times = Vec::new(); let mut sla_violations = 0; let mut performance_regressions = 0; let mut checkpoint_failures = 0; // Process general chaos results for result in chaos_results { if let Some(recovery_time) = result.recovery_time_ms { total_recovery_time_ms += recovery_time; recovery_times.push(recovery_time); // Check for SLA violations (assuming 100ms max recovery time for HFT) if recovery_time > 100 { sla_violations += 1; } } if result.performance_regression.is_some() { performance_regressions += 1; } if !result.checkpoint_integrity { checkpoint_failures += 1; } } // Process ML-specific results for ml_result in ml_results { if !ml_result.training_loss_continuity { checkpoint_failures += 1; } if ml_result.performance_regression.is_some() { performance_regressions += 1; } } let average_recovery_time_ms = if !recovery_times.is_empty() { total_recovery_time_ms as f64 / recovery_times.len() as f64 } else { 0.0 }; let max_recovery_time_ms = recovery_times.into_iter().max().unwrap_or(0); PerformanceSummary { total_recovery_time_ms, average_recovery_time_ms, max_recovery_time_ms, sla_violations, performance_regressions, checkpoint_failures, } } /// Generate and save chaos job report async fn generate_and_save_report(&self, job_id: Uuid) -> Result<()> { let job = { let history = self.job_history.read().await; history .iter() .find(|j| j.id == job_id) .cloned() .ok_or_else(|| anyhow::anyhow!("Job not found: {}", job_id))? }; let config = self.config.read().await.clone(); // Generate ML chaos report let ml_chaos_tests = MLTrainingChaosTests::new(config.ml_chaos_config.clone()); let ml_report = ml_chaos_tests .generate_chaos_report(&job.ml_chaos_results) .await?; // Generate comprehensive report let mut full_report = String::new(); full_report.push_str("# Nightly Chaos Engineering Report\n\n"); full_report.push_str(&format!("**Job ID:** {}\n", job.id)); full_report.push_str(&format!("**Scheduled:** {}\n", job.scheduled_at)); full_report.push_str(&format!( "**Started:** {}\n", job.started_at.map_or("N/A".to_string(), |t| t.to_string()) )); full_report.push_str(&format!( "**Completed:** {}\n", job.completed_at .map_or("N/A".to_string(), |t| t.to_string()) )); full_report.push_str(&format!("**Status:** {:?}\n\n", job.status)); // Performance summary if let Some(ref summary) = job.performance_summary { full_report.push_str("## Performance Summary\n"); full_report.push_str(&format!( "- **Average Recovery Time:** {:.1}ms\n", summary.average_recovery_time_ms )); full_report.push_str(&format!( "- **Max Recovery Time:** {}ms\n", summary.max_recovery_time_ms )); full_report.push_str(&format!( "- **SLA Violations:** {}\n", summary.sla_violations )); full_report.push_str(&format!( "- **Performance Regressions:** {}\n", summary.performance_regressions )); full_report.push_str(&format!( "- **Checkpoint Failures:** {}\n\n", summary.checkpoint_failures )); } // Add ML-specific report full_report.push_str(&ml_report); // Add error messages if any if !job.error_messages.is_empty() { full_report.push_str("\n## Error Messages\n"); for error in &job.error_messages { full_report.push_str(&format!("- {}\n", error)); } } // Save report to file let report_filename = format!( "chaos_report_{}_{}.md", job_id, job.scheduled_at.format("%Y%m%d_%H%M%S") ); let report_path = config.report_storage_path.join(report_filename); fs::write(&report_path, full_report) .await .context("Failed to save chaos report")?; // Update job with report path self.update_job_report_path(job_id, Some(report_path)) .await?; Ok(()) } /// Check for alerts and send notifications async fn check_and_send_alerts(&self, job_id: Uuid, summary: &PerformanceSummary) { let config = self.config.read().await.clone(); // Check for critical alerts if summary.sla_violations > 0 { let message = format!( "🚨 CRITICAL: {} SLA violations detected in chaos job {}. Max recovery time: {}ms", summary.sla_violations, job_id, summary.max_recovery_time_ms ); let _ = self._event_sender.send(ChaosJobEvent::AlertTriggered { message: message.clone(), severity: AlertSeverity::Critical, }); if let Some(ref webhook) = config.notification_webhook { self.send_webhook_notification(webhook, &message, AlertSeverity::Critical) .await; } } // Check for warning alerts if summary.checkpoint_failures > 0 { let message = format!( "⚠️ WARNING: {} checkpoint failures detected in chaos job {}", summary.checkpoint_failures, job_id ); let _ = self._event_sender.send(ChaosJobEvent::AlertTriggered { message: message.clone(), severity: AlertSeverity::Warning, }); if let Some(ref webhook) = config.notification_webhook { self.send_webhook_notification(webhook, &message, AlertSeverity::Warning) .await; } } // Send success notification if summary.sla_violations == 0 && summary.checkpoint_failures == 0 { let message = format!( "✅ Chaos job {} completed successfully. Avg recovery time: {:.1}ms", job_id, summary.average_recovery_time_ms ); if let Some(ref webhook) = config.notification_webhook { self.send_webhook_notification(webhook, &message, AlertSeverity::Info) .await; } } } /// Send webhook notification async fn send_webhook_notification( &self, webhook_url: &str, message: &str, severity: AlertSeverity, ) { // TODO: Implement actual webhook sending (Slack, Teams, etc.) info!( "Sending {} alert: {}", match severity { AlertSeverity::Info => "INFO", AlertSeverity::Warning => "WARNING", AlertSeverity::Critical => "CRITICAL", }, message ); } /// Subscribe to chaos job events pub fn subscribe_events(&self) -> broadcast::Receiver { self._event_sender.subscribe() } /// Get job history pub async fn get_job_history(&self) -> Vec { self.job_history.read().await.clone() } /// Update configuration pub async fn update_config(&self, new_config: NightlyChaosConfig) { let mut config = self.config.write().await; *config = new_config; info!("Updated nightly chaos configuration"); } // Helper methods for updating job state async fn update_job_status(&self, job_id: Uuid, status: ChaosJobStatus) -> Result<()> { let mut history = self.job_history.write().await; if let Some(job) = history.iter_mut().find(|j| j.id == job_id) { job.status = status; } Ok(()) } async fn update_job_start_time( &self, job_id: Uuid, start_time: Option>, ) -> Result<()> { let mut history = self.job_history.write().await; if let Some(job) = history.iter_mut().find(|j| j.id == job_id) { job.started_at = start_time; } Ok(()) } async fn update_job_completion_time( &self, job_id: Uuid, completion_time: Option>, ) -> Result<()> { let mut history = self.job_history.write().await; if let Some(job) = history.iter_mut().find(|j| j.id == job_id) { job.completed_at = completion_time; } Ok(()) } async fn update_job_ml_results( &self, job_id: Uuid, ml_results: Vec, ) -> Result<()> { let mut history = self.job_history.write().await; if let Some(job) = history.iter_mut().find(|j| j.id == job_id) { job.ml_chaos_results = ml_results; job.total_experiments = job.chaos_results.len() + job.ml_chaos_results.len(); job.successful_experiments = job .ml_chaos_results .iter() .filter(|r| r.training_loss_continuity) .count(); job.failed_experiments = job.total_experiments - job.successful_experiments; } Ok(()) } async fn update_job_performance_summary( &self, job_id: Uuid, summary: Option, ) -> Result<()> { let mut history = self.job_history.write().await; if let Some(job) = history.iter_mut().find(|j| j.id == job_id) { job.performance_summary = summary; } Ok(()) } async fn update_job_report_path( &self, job_id: Uuid, report_path: Option, ) -> Result<()> { let mut history = self.job_history.write().await; if let Some(job) = history.iter_mut().find(|j| j.id == job_id) { job.report_path = report_path; } Ok(()) } async fn add_job_error(&self, job_id: Uuid, error: String) -> Result<()> { let mut history = self.job_history.write().await; if let Some(job) = history.iter_mut().find(|j| j.id == job_id) { job.error_messages.push(error); } Ok(()) } } impl Clone for NightlyChaosRunner { fn clone(&self) -> Self { Self { config: Arc::clone(&self.config), job_history: Arc::clone(&self.job_history), _event_sender: self._event_sender.clone(), is_running: Arc::clone(&self.is_running), } } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_nightly_chaos_runner() { let config = NightlyChaosConfig::default(); let runner = NightlyChaosRunner::new(config); // Test event subscription let mut event_receiver = runner.subscribe_events(); assert!(event_receiver.try_recv().is_err()); // No events yet // Test job history let history = runner.get_job_history().await; assert!(history.is_empty()); } #[test] fn test_performance_summary() { let ml_results = vec![MLChaosResult { experiment_id: Uuid::new_v4(), model_type: super::ml_training_chaos::ModelType::TLOB, training_job_id: Some("test_job".to_string()), checkpoint_before_failure: None, checkpoint_after_recovery: None, model_accuracy_before: None, model_accuracy_after: None, training_loss_continuity: true, gpu_memory_recovery: None, performance_regression: None, }]; let config = NightlyChaosConfig::default(); let runner = NightlyChaosRunner::new(config); let summary = runner.calculate_performance_summary(&[], &ml_results); assert_eq!(summary.checkpoint_failures, 0); } }