//! Comprehensive Chaos Engineering Framework for Foxhunt HFT System //! //! This framework implements systematic failure injection, recovery validation, //! and checkpoint resume testing specifically designed for HFT requirements. use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::process::{Child, Command, Stdio}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::{broadcast, RwLock, Semaphore}; use tokio::time::{sleep, timeout}; use tracing::{error, info, warn}; use uuid::Uuid; /// Configuration for a chaos engineering experiment /// /// Defines all parameters needed to execute a controlled failure injection /// and recovery validation test for HFT trading system components. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChaosExperiment { /// Unique identifier for this experiment pub id: Uuid, /// Human-readable name for the experiment pub name: String, /// Detailed description of what the experiment tests pub description: String, /// Name of the service or component being tested pub target_service: String, /// Type of failure to inject pub failure_type: FailureType, /// How long to maintain the failure condition pub duration: Duration, /// Maximum time to wait for service recovery pub recovery_timeout: Duration, /// HFT requirement: sub-100ms recovery time pub max_recovery_time_ms: u64, /// Whether this experiment is enabled for execution pub enabled: bool, } /// Types of failures that can be injected during chaos testing /// /// Covers the primary failure modes that HFT systems must handle: /// process crashes, resource exhaustion, network issues, and external dependencies. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum FailureType { /// Kill the target process and optionally restart it ProcessKill { /// Signal to send to the process signal: Signal, /// Delay before restarting the service (ms) delay_before_restart_ms: u64, }, /// Create memory pressure to test OOM handling MemoryPressure { /// Amount of memory to allocate (MB) target_mb: u64, /// How long to maintain pressure (ms) duration_ms: u64, }, /// Create network partition by blocking specific ports NetworkPartition { /// Ports to block traffic to/from target_ports: Vec, /// Duration of the partition (ms) duration_ms: u64, }, /// Inject disk I/O failures for specific paths DiskIoFailure { /// File system paths to affect target_paths: Vec, /// Percentage of I/O operations to fail failure_rate_percent: u8, }, /// Throttle CPU usage to test performance degradation CpuThrottle { /// CPU usage limit as percentage cpu_limit_percent: u8, /// Duration of throttling (ms) duration_ms: u64, }, /// Exhaust GPU memory resources for ML service testing GpuResourceExhaustion { /// Percentage of GPU memory to fill memory_fill_percent: u8, /// Duration of resource exhaustion (ms) duration_ms: u64, }, /// Simulate database connection failures DatabaseConnectionFailure { /// Database connection string to target connection_string: String, /// Duration of connection failure (ms) duration_ms: u64, }, } /// Unix signals that can be sent to processes #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Signal { /// Graceful termination signal SIGTERM, /// Forceful kill signal (non-catchable) SIGKILL, /// Stop process execution (can be resumed) SIGSTOP, /// Continue stopped process SIGCONT, } /// Results from executing a chaos engineering experiment /// /// Contains all metrics and outcomes from the experiment including /// recovery time, checkpoint integrity, and performance impact. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChaosResult { /// ID of the experiment that generated this result pub experiment_id: Uuid, /// When the experiment started pub started_at: std::time::SystemTime, /// When the experiment completed (None if still running) pub completed_at: Option, /// Final status of the experiment pub status: ChaosStatus, /// Time taken for service recovery in milliseconds pub recovery_time_ms: Option, /// Whether checkpoint data remained valid through the failure pub checkpoint_integrity: bool, /// Performance impact measured after recovery pub performance_regression: Option, /// Any errors encountered during the experiment pub errors: Vec, } /// Status of a chaos experiment execution #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ChaosStatus { /// Experiment is currently executing Running, /// Experiment completed successfully with all validations passed Succeeded, /// Experiment failed during execution Failed, /// Service failed to recover within the specified timeout RecoveryTimeout, /// Checkpoint data was corrupted during the failure CheckpointCorrupted, } /// Performance regression metrics comparing before/after experiment #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PerformanceRegression { /// 99th percentile latency before the experiment (nanoseconds) pub latency_p99_before_ns: u64, /// 99th percentile latency after recovery (nanoseconds) pub latency_p99_after_ns: u64, /// Percentage increase in latency (negative if improved) pub regression_percent: f64, } /// Main chaos engineering orchestrator for coordinating experiments /// /// Manages experiment registration, execution, resource limits, and result tracking. /// /// Ensures safe concurrent execution with proper cleanup and monitoring. pub struct ChaosOrchestrator { /// Registry of all configured experiments experiments: Arc>>, /// Currently executing experiments active_experiments: Arc>>, /// Historical results from all experiments results: Arc>>, /// Event broadcaster for real-time notifications _event_sender: broadcast::Sender, /// Semaphore limiting concurrent experiment execution max_concurrent_experiments: Arc, } /// Internal state tracking for an executing chaos experiment struct ChaosExecution { /// Child processes spawned during the experiment child_processes: Vec, /// When the experiment started executing start_time: Instant, /// When recovery phase began (if applicable) recovery_start: Option, /// Path to checkpoint file (if created) checkpoint_path: Option, } /// Events emitted during chaos experiment execution /// /// These events can be subscribed to for real-time monitoring /// and alerting during chaos engineering runs. #[derive(Debug, Clone)] pub enum ChaosEvent { /// An experiment has begun execution ExperimentStarted { /// Experiment ID id: Uuid, /// Experiment name name: String, }, /// An experiment has completed (successfully or not) ExperimentCompleted { /// Experiment ID id: Uuid, /// Final result of the experiment result: ChaosResult, }, /// Service recovery phase has begun RecoveryStarted { /// Experiment ID id: Uuid, /// Service name being recovered service: String, }, /// Checkpoint validation has completed CheckpointValidated { /// Experiment ID id: Uuid, /// Whether checkpoint is valid valid: bool, }, /// Performance regression detected after recovery PerformanceRegression { /// Experiment ID id: Uuid, /// Regression details regression: PerformanceRegression, }, } impl ChaosOrchestrator { /// Create a new chaos orchestrator with concurrency limits /// /// # Arguments /// * `max_concurrent` - Maximum number of experiments to run simultaneously /// /// # Returns /// /// A new ChaosOrchestrator instance ready to execute experiments pub fn new(max_concurrent: usize) -> Self { let (_event_sender, _) = broadcast::channel(1000); Self { experiments: Arc::new(RwLock::new(HashMap::new())), active_experiments: Arc::new(RwLock::new(HashMap::new())), results: Arc::new(RwLock::new(Vec::new())), _event_sender, max_concurrent_experiments: Arc::new(Semaphore::new(max_concurrent)), } } /// Register a new chaos experiment for future execution /// /// # Arguments /// * `experiment` - The chaos experiment configuration to register /// /// # Returns /// * `Ok(())` - If experiment was successfully registered /// /// * `Err(anyhow::Error)` - If registration failed pub async fn register_experiment(&self, experiment: ChaosExperiment) -> Result<()> { let mut experiments = self.experiments.write().await; experiments.insert(experiment.id, experiment); Ok(()) } /// Execute a specific chaos experiment by ID /// /// Runs the complete chaos experiment lifecycle including failure injection, /// recovery validation, checkpoint verification, and performance measurement. /// /// # Arguments /// * `experiment_id` - UUID of the experiment to execute /// /// # Returns /// * `Ok(ChaosResult)` - Complete results of the experiment /// /// * `Err(anyhow::Error)` - If experiment execution failed pub async fn execute_experiment(&self, experiment_id: Uuid) -> Result { // Acquire semaphore to limit concurrent experiments let _permit = self.max_concurrent_experiments.acquire().await?; let experiment = { let experiments = self.experiments.read().await; experiments .get(&experiment_id) .ok_or_else(|| anyhow::anyhow!("Experiment not found: {}", experiment_id))? .clone() }; if !experiment.enabled { return Err(anyhow::anyhow!( "Experiment {} is disabled", experiment.name )); } info!("Starting chaos experiment: {}", experiment.name); // Send start event let _ = self._event_sender.send(ChaosEvent::ExperimentStarted { id: experiment.id, name: experiment.name.clone(), }); let mut result = ChaosResult { experiment_id: experiment.id, started_at: std::time::SystemTime::now(), completed_at: None, status: ChaosStatus::Running, recovery_time_ms: None, checkpoint_integrity: false, performance_regression: None, errors: Vec::new(), }; // Execute the chaos experiment match self.run_chaos_experiment(&experiment).await { Ok(execution_result) => { result.status = execution_result.status; result.recovery_time_ms = execution_result.recovery_time_ms; result.checkpoint_integrity = execution_result.checkpoint_integrity; result.performance_regression = execution_result.performance_regression; } Err(e) => { result.status = ChaosStatus::Failed; result.errors.push(e.to_string()); error!("Chaos experiment failed: {}", e); } } result.completed_at = Some(std::time::SystemTime::now()); // Store result { let mut results = self.results.write().await; results.push(result.clone()); } // Send completion event let _ = self._event_sender.send(ChaosEvent::ExperimentCompleted { id: experiment.id, result: result.clone(), }); info!( "Chaos experiment completed: {} - Status: {:?}", experiment.name, result.status ); Ok(result) } /// Execute the actual chaos experiment async fn run_chaos_experiment(&self, experiment: &ChaosExperiment) -> Result { let start_time = Instant::now(); // Step 1: Capture baseline performance metrics let baseline_metrics = self .capture_performance_metrics(&experiment.target_service) .await?; // Step 2: Create checkpoint if applicable let checkpoint_path = self.create_checkpoint(&experiment.target_service).await?; // Step 3: Inject failure info!("Injecting failure: {:?}", experiment.failure_type); self.inject_failure(&experiment.failure_type, &experiment.target_service) .await?; // Step 4: Wait for failure duration sleep(experiment.duration).await; // Step 5: Begin recovery process let recovery_start = Instant::now(); let _ = self._event_sender.send(ChaosEvent::RecoveryStarted { id: experiment.id, service: experiment.target_service.clone(), }); // Step 6: Validate service recovery within timeout let recovery_result = timeout( experiment.recovery_timeout, self.wait_for_service_recovery(&experiment.target_service), ) .await; let recovery_time_ms = recovery_start.elapsed().as_millis() as u64; // Step 7: Validate checkpoint integrity if applicable let checkpoint_integrity = if let Some(ref path) = checkpoint_path { self.validate_checkpoint(&experiment.target_service, path) .await? } else { true // No checkpoint to validate }; let _ = self._event_sender.send(ChaosEvent::CheckpointValidated { id: experiment.id, valid: checkpoint_integrity, }); // Step 8: Measure post-recovery performance let post_metrics = self .capture_performance_metrics(&experiment.target_service) .await?; // Step 9: Calculate performance regression let performance_regression = self.calculate_performance_regression(&baseline_metrics, &post_metrics); if let Some(ref regression) = performance_regression { let _ = self._event_sender.send(ChaosEvent::PerformanceRegression { id: experiment.id, regression: regression.clone(), }); } // Determine final status let status = match recovery_result { Ok(Ok(true)) if checkpoint_integrity && recovery_time_ms <= experiment.max_recovery_time_ms => { ChaosStatus::Succeeded } Ok(Ok(true)) if !checkpoint_integrity => ChaosStatus::CheckpointCorrupted, Ok(Ok(true)) => { ChaosStatus::Failed // Recovery took too long } Ok(Ok(false)) | Ok(Err(_)) | Err(_) => ChaosStatus::RecoveryTimeout, }; Ok(ExecutionResult { status, recovery_time_ms: Some(recovery_time_ms), checkpoint_integrity, performance_regression, }) } /// Inject specific type of failure async fn inject_failure(&self, failure_type: &FailureType, service: &str) -> Result<()> { match failure_type { FailureType::ProcessKill { signal, delay_before_restart_ms, } => { self.kill_service_process(service, signal).await?; sleep(Duration::from_millis(*delay_before_restart_ms)).await; self.restart_service(service).await?; } FailureType::MemoryPressure { target_mb, duration_ms, } => { self.inject_memory_pressure(*target_mb, *duration_ms) .await?; } FailureType::NetworkPartition { target_ports, duration_ms, } => { self.create_network_partition(target_ports, *duration_ms) .await?; } FailureType::DiskIoFailure { target_paths, failure_rate_percent, } => { self.inject_disk_failures(target_paths, *failure_rate_percent) .await?; } FailureType::CpuThrottle { cpu_limit_percent, duration_ms, } => { self.throttle_cpu(*cpu_limit_percent, *duration_ms).await?; } FailureType::GpuResourceExhaustion { memory_fill_percent, duration_ms, } => { self.exhaust_gpu_resources(*memory_fill_percent, *duration_ms) .await?; } FailureType::DatabaseConnectionFailure { connection_string, duration_ms, } => { self.inject_db_connection_failure(connection_string, *duration_ms) .await?; } } Ok(()) } /// Kill service process with specified signal async fn kill_service_process(&self, service: &str, signal: &Signal) -> Result<()> { let signal_arg = match signal { Signal::SIGTERM => "-TERM", Signal::SIGKILL => "-KILL", Signal::SIGSTOP => "-STOP", Signal::SIGCONT => "-CONT", }; let output = Command::new("pkill") .args([signal_arg, service]) .output() .context("Failed to kill service process")?; if !output.status.success() { warn!( "pkill command failed: {}", String::from_utf8_lossy(&output.stderr) ); } Ok(()) } /// Restart service process async fn restart_service(&self, service: &str) -> Result<()> { let output = Command::new("systemctl") .args(["restart", service]) .output() .context("Failed to restart service")?; if !output.status.success() { return Err(anyhow::anyhow!( "Service restart failed: {}", String::from_utf8_lossy(&output.stderr) )); } Ok(()) } /// Wait for service to recover and be healthy async fn wait_for_service_recovery(&self, service: &str) -> Result { const MAX_ATTEMPTS: u32 = 30; const DELAY_BETWEEN_ATTEMPTS: Duration = Duration::from_secs(1); for attempt in 1..=MAX_ATTEMPTS { if self.is_service_healthy(service).await? { info!("Service {} recovered after {} attempts", service, attempt); return Ok(true); } if attempt < MAX_ATTEMPTS { sleep(DELAY_BETWEEN_ATTEMPTS).await; } } warn!( "Service {} failed to recover within {} attempts", service, MAX_ATTEMPTS ); Ok(false) } /// Check if service is healthy via health check endpoint async fn is_service_healthy(&self, service: &str) -> Result { // This would typically make HTTP/gRPC health check calls // For now, checking if process is running let output = Command::new("pgrep") .arg(service) .output() .context("Failed to check service process")?; Ok(output.status.success()) } /// Create checkpoint for ML service async fn create_checkpoint(&self, service: &str) -> Result> { if service.contains("ml") { let checkpoint_path = format!( "/tmp/chaos_checkpoint_{}_{}", service, std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs() ); // Trigger checkpoint creation via gRPC call // This would call the ML service's create_checkpoint method info!("Creating checkpoint at: {}", checkpoint_path); // TODO: Implement actual checkpoint creation via gRPC Ok(Some(checkpoint_path)) } else { Ok(None) } } /// Validate checkpoint integrity async fn validate_checkpoint(&self, service: &str, checkpoint_path: &str) -> Result { info!("Validating checkpoint: {}", checkpoint_path); // TODO: Implement checkpoint validation logic // This would: // 1. Check file integrity // 2. Validate model state consistency // 3. Ensure all required files are present // 4. Test checkpoint loading Ok(true) // Placeholder } /// Capture performance metrics async fn capture_performance_metrics(&self, service: &str) -> Result { // TODO: Implement actual metrics capture from Prometheus/monitoring Ok(PerformanceMetrics { latency_p99_ns: 50000, // 50μs placeholder }) } /// Calculate performance regression fn calculate_performance_regression( &self, baseline: &PerformanceMetrics, current: &PerformanceMetrics, ) -> Option { if current.latency_p99_ns > baseline.latency_p99_ns { let regression_percent = ((current.latency_p99_ns as f64 - baseline.latency_p99_ns as f64) / baseline.latency_p99_ns as f64) * 100.0; Some(PerformanceRegression { latency_p99_before_ns: baseline.latency_p99_ns, latency_p99_after_ns: current.latency_p99_ns, regression_percent, }) } else { None } } /// Inject memory pressure async fn inject_memory_pressure(&self, target_mb: u64, duration_ms: u64) -> Result<()> { info!( "Injecting memory pressure: {}MB for {}ms", target_mb, duration_ms ); // Use stress-ng or similar tool to create memory pressure let mut child = Command::new("stress-ng") .args([ "--vm", "1", "--vm-bytes", &format!("{}M", target_mb), "--timeout", &format!("{}ms", duration_ms), ]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .context("Failed to start memory stress test")?; let _ = child.wait(); Ok(()) } /// Create network partition async fn create_network_partition(&self, ports: &[u16], duration_ms: u64) -> Result<()> { info!( "Creating network partition for ports {:?} for {}ms", ports, duration_ms ); // Use iptables to block traffic to specific ports for port in ports { Command::new("iptables") .args([ "-A", "OUTPUT", "-p", "tcp", "--dport", &port.to_string(), "-j", "DROP", ]) .output() .context("Failed to create network partition")?; } // Wait for specified duration sleep(Duration::from_millis(duration_ms)).await; // Remove iptables rules for port in ports { let _ = Command::new("iptables") .args([ "-D", "OUTPUT", "-p", "tcp", "--dport", &port.to_string(), "-j", "DROP", ]) .output(); } Ok(()) } /// Inject disk I/O failures async fn inject_disk_failures(&self, paths: &[String], failure_rate: u8) -> Result<()> { info!( "Injecting disk failures for paths {:?} at {}% rate", paths, failure_rate ); // TODO: Implement disk I/O failure injection // This could use fault injection tools or filesystem manipulation Ok(()) } /// Throttle CPU usage async fn throttle_cpu(&self, limit_percent: u8, duration_ms: u64) -> Result<()> { info!("Throttling CPU to {}% for {}ms", limit_percent, duration_ms); // Use cgroups or cpulimit to throttle CPU let mut child = Command::new("cpulimit") .args(["-l", &limit_percent.to_string(), "-p", "1"]) // Target init process .spawn() .context("Failed to start CPU throttling")?; sleep(Duration::from_millis(duration_ms)).await; let _ = child.kill(); Ok(()) } /// Exhaust GPU resources async fn exhaust_gpu_resources(&self, memory_fill_percent: u8, duration_ms: u64) -> Result<()> { info!( "Exhausting GPU resources: {}% memory for {}ms", memory_fill_percent, duration_ms ); // TODO: Implement GPU memory exhaustion // This would allocate GPU memory to simulate resource exhaustion Ok(()) } /// Inject database connection failures async fn inject_db_connection_failure( &self, connection_string: &str, duration_ms: u64, ) -> Result<()> { info!( "Injecting DB connection failure for {} for {}ms", connection_string, duration_ms ); // TODO: Implement database connection failure injection // This could involve firewall rules or connection pool manipulation Ok(()) } /// Get results from all executed experiments /// /// # Returns /// /// Vector containing results from all completed experiments pub async fn get_results(&self) -> Vec { self.results.read().await.clone() } /// Subscribe to real-time chaos experiment events /// /// # Returns /// /// A broadcast receiver for monitoring experiment progress and events pub fn subscribe_events(&self) -> broadcast::Receiver { self._event_sender.subscribe() } } /// Internal result structure for chaos experiment execution #[derive(Debug)] struct ExecutionResult { /// Final status of the experiment status: ChaosStatus, /// Time taken for recovery in milliseconds recovery_time_ms: Option, /// Whether checkpoint remained valid checkpoint_integrity: bool, /// Any performance regression detected performance_regression: Option, } /// Performance metrics captured during chaos experiments #[derive(Debug)] struct PerformanceMetrics { /// 99th percentile latency in nanoseconds latency_p99_ns: u64, } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_chaos_orchestrator_creation() { let orchestrator = ChaosOrchestrator::new(3); let experiment = ChaosExperiment { id: Uuid::new_v4(), name: "MLTrainingService Kill Test".to_string(), description: "Test MLTrainingService recovery from process kill".to_string(), target_service: "ml_training_service".to_string(), failure_type: FailureType::ProcessKill { signal: Signal::SIGTERM, delay_before_restart_ms: 1000, }, duration: Duration::from_secs(10), recovery_timeout: Duration::from_secs(30), max_recovery_time_ms: 100, // HFT requirement enabled: true, }; assert!(orchestrator.register_experiment(experiment).await.is_ok()); } }