Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
805 lines
26 KiB
Rust
805 lines
26 KiB
Rust
//! 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<String>, // 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<Utc>,
|
|
pub started_at: Option<DateTime<Utc>>,
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
pub status: ChaosJobStatus,
|
|
pub chaos_results: Vec<ChaosResult>,
|
|
pub ml_chaos_results: Vec<MLChaosResult>,
|
|
pub total_experiments: usize,
|
|
pub successful_experiments: usize,
|
|
pub failed_experiments: usize,
|
|
pub report_path: Option<PathBuf>,
|
|
pub error_messages: Vec<String>,
|
|
pub performance_summary: Option<PerformanceSummary>,
|
|
}
|
|
|
|
#[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<RwLock<NightlyChaosConfig>>,
|
|
job_history: Arc<RwLock<Vec<ChaosJobExecution>>>,
|
|
_event_sender: broadcast::Sender<ChaosJobEvent>,
|
|
is_running: Arc<RwLock<bool>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ChaosJobEvent {
|
|
JobScheduled {
|
|
id: Uuid,
|
|
scheduled_time: DateTime<Utc>,
|
|
},
|
|
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<Uuid> {
|
|
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<PerformanceSummary> {
|
|
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<ChaosJobEvent> {
|
|
self._event_sender.subscribe()
|
|
}
|
|
|
|
/// Get job history
|
|
pub async fn get_job_history(&self) -> Vec<ChaosJobExecution> {
|
|
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<DateTime<Utc>>,
|
|
) -> 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<DateTime<Utc>>,
|
|
) -> 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<MLChaosResult>,
|
|
) -> 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<PerformanceSummary>,
|
|
) -> 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<PathBuf>,
|
|
) -> 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);
|
|
}
|
|
}
|