//! State Transition Storm Stress Tests //! //! Tests rapid state transitions under high concurrent load. //! Validates database trigger performance, index efficiency, and deadlock prevention. use anyhow::Result; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; use uuid::Uuid; use ml_training_service::orchestrator::{JobStatus, TrainingJob}; use ml::training_pipeline::ProductionTrainingConfig; /// Helper to create a minimal training config fn create_test_config() -> ProductionTrainingConfig { ProductionTrainingConfig::default() } /// Test 1: 1000 Concurrent Status Updates /// /// Validates database can handle 1000 concurrent UPDATE operations /// without deadlocks or excessive lock contention. #[tokio::test] #[ignore = "Stress test - run explicitly with --ignored"] async fn test_1000_concurrent_status_updates() -> Result<()> { println!("\n=== Test 1: 1000 Concurrent Status Updates ==="); let start = Instant::now(); let jobs = Arc::new(RwLock::new(std::collections::HashMap::new())); // Create 100 jobs for i in 0..100 { let config = create_test_config(); let job = TrainingJob::new( "DQN".to_string(), config, format!("Job {}", i), std::collections::HashMap::new(), ); jobs.write().await.insert(job.id, job); } let success_count = Arc::new(AtomicU32::new(0)); let conflict_count = Arc::new(AtomicU32::new(0)); let mut handles = Vec::new(); // Perform 1000 status updates (10 per job) for _ in 0..1000 { let jobs_clone = jobs.clone(); let success = success_count.clone(); let conflicts = conflict_count.clone(); let handle = tokio::spawn(async move { // Pick random job let job_ids: Vec = { let jobs_read = jobs_clone.read().await; jobs_read.keys().copied().collect() }; if let Some(&job_id) = job_ids.get(rand::random::() % job_ids.len()) { // Update status let mut jobs_write = jobs_clone.write().await; if let Some(job) = jobs_write.get_mut(&job_id) { // Simulate status update job.status = JobStatus::Running; job.progress_percentage = rand::random::() * 100.0; success.fetch_add(1, Ordering::Relaxed); } else { conflicts.fetch_add(1, Ordering::Relaxed); } } }); handles.push(handle); } // Wait for all updates for handle in handles { handle.await.ok(); } let elapsed = start.elapsed(); let success = success_count.load(Ordering::Relaxed); let conflicts = conflict_count.load(Ordering::Relaxed); println!("✓ Test 1 Results:"); println!(" - Duration: {:?}", elapsed); println!(" - Successful updates: {}/1000", success); println!(" - Conflicts: {}", conflicts); println!(" - Throughput: {:.0} updates/sec", 1000.0 / elapsed.as_secs_f64()); assert!(success >= 950, "Expected at least 950 successful updates, got {}", success); assert!(elapsed < Duration::from_secs(5), "Expected completion under 5s, got {:?}", elapsed); Ok(()) } /// Test 2: Rapid State Transitions (Pending → Running → Completed) /// /// Tests state machine integrity under rapid transitions. #[tokio::test] #[ignore = "Stress test - run explicitly with --ignored"] async fn test_rapid_state_transitions() -> Result<()> { println!("\n=== Test 2: Rapid State Transitions ==="); let start = Instant::now(); let jobs = Arc::new(RwLock::new(std::collections::HashMap::new())); // Create 100 jobs in Pending state let mut job_ids = Vec::new(); for i in 0..100 { let config = create_test_config(); let job = TrainingJob::new( "DQN".to_string(), config, format!("Transition job {}", i), std::collections::HashMap::new(), ); let job_id = job.id; jobs.write().await.insert(job_id, job); job_ids.push(job_id); } let completed_count = Arc::new(AtomicU32::new(0)); let mut handles = Vec::new(); // Transition each job: Pending → Running → Completed for job_id in job_ids { let jobs_clone = jobs.clone(); let completed = completed_count.clone(); let handle = tokio::spawn(async move { // Pending → Running { let mut jobs_write = jobs_clone.write().await; if let Some(job) = jobs_write.get_mut(&job_id) { job.status = JobStatus::Running; job.started_at = Some(chrono::Utc::now()); } } tokio::time::sleep(Duration::from_micros(100)).await; // Running → Completed { let mut jobs_write = jobs_clone.write().await; if let Some(job) = jobs_write.get_mut(&job_id) { job.status = JobStatus::Completed; job.completed_at = Some(chrono::Utc::now()); job.progress_percentage = 100.0; completed.fetch_add(1, Ordering::Relaxed); } } }); handles.push(handle); } // Wait for all transitions for handle in handles { handle.await.ok(); } let elapsed = start.elapsed(); let completed = completed_count.load(Ordering::Relaxed); // Verify final states let jobs_read = jobs.read().await; let completed_jobs = jobs_read.values() .filter(|j| j.status == JobStatus::Completed) .count(); println!("✓ Test 2 Results:"); println!(" - Duration: {:?}", elapsed); println!(" - Completed transitions: {}/100", completed); println!(" - Final completed jobs: {}/100", completed_jobs); println!(" - Avg transition time: {:?}", elapsed / 100); assert_eq!(completed, 100, "Expected all 100 jobs to complete transitions"); assert_eq!(completed_jobs, 100, "Expected all jobs in Completed state"); Ok(()) } /// Test 3: Database Trigger Performance (1000 Updates/Sec) /// /// Simulates database triggers firing on status updates. /// Validates trigger execution time stays within acceptable bounds. #[tokio::test] #[ignore = "Stress test - run explicitly with --ignored"] async fn test_database_trigger_performance() -> Result<()> { println!("\n=== Test 3: Database Trigger Performance ==="); let start = Instant::now(); let trigger_execution_count = Arc::new(AtomicU32::new(0)); let trigger_execution_time = Arc::new(std::sync::Mutex::new(Duration::ZERO)); let mut handles = Vec::new(); // Simulate 1000 updates with trigger execution for i in 0..1000 { let exec_count = trigger_execution_count.clone(); let exec_time = trigger_execution_time.clone(); let handle = tokio::spawn(async move { let trigger_start = Instant::now(); // Simulate trigger logic (update audit log, send notification, etc.) let config = create_test_config(); let mut job = TrainingJob::new( "DQN".to_string(), config, format!("Trigger test {}", i), std::collections::HashMap::new(), ); job.status = JobStatus::Running; job.progress_percentage = (i % 100) as f32; // Simulate trigger overhead (1-5ms typical) tokio::time::sleep(Duration::from_micros(500)).await; let trigger_elapsed = trigger_start.elapsed(); exec_count.fetch_add(1, Ordering::Relaxed); let mut total_time = exec_time.lock().expect("INVARIANT: Lock should not be poisoned"); *total_time += trigger_elapsed; }); handles.push(handle); } // Wait for all triggers for handle in handles { handle.await.ok(); } let elapsed = start.elapsed(); let exec_count = trigger_execution_count.load(Ordering::Relaxed); let total_trigger_time = *trigger_execution_time.lock().expect("INVARIANT: Lock should not be poisoned"); let avg_trigger_time = total_trigger_time / exec_count; let throughput = exec_count as f64 / elapsed.as_secs_f64(); println!("✓ Test 3 Results:"); println!(" - Duration: {:?}", elapsed); println!(" - Trigger executions: {}/1000", exec_count); println!(" - Avg trigger time: {:?}", avg_trigger_time); println!(" - Throughput: {:.0} updates/sec", throughput); println!(" - Target: >1000 updates/sec"); assert_eq!(exec_count, 1000, "Expected all 1000 triggers to execute"); assert!(throughput >= 1000.0, "Throughput {:.0}/sec below 1000/sec target", throughput); assert!(avg_trigger_time < Duration::from_millis(5), "Avg trigger time {:?} exceeds 5ms", avg_trigger_time); Ok(()) } /// Test 4: Index Performance Under Load /// /// Validates that database indexes remain efficient under heavy load. /// Tests common query patterns (filter by status, sort by created_at). #[tokio::test] #[ignore = "Stress test - run explicitly with --ignored"] async fn test_index_performance_under_load() -> Result<()> { println!("\n=== Test 4: Index Performance Under Load ==="); let start = Instant::now(); let jobs = Arc::new(RwLock::new(std::collections::HashMap::new())); // Create 1000 jobs with varying states for i in 0..1000 { let config = create_test_config(); let mut job = TrainingJob::new( "DQN".to_string(), config, format!("Index test {}", i), std::collections::HashMap::new(), ); // Distribute across states job.status = match i % 5 { 0 => JobStatus::Pending, 1 => JobStatus::Running, 2 => JobStatus::Completed, 3 => JobStatus::Failed, _ => JobStatus::Stopped, }; jobs.write().await.insert(job.id, job); } let query_count = Arc::new(AtomicU32::new(0)); let query_times = Arc::new(std::sync::Mutex::new(Vec::new())); let mut handles = Vec::new(); // Perform 100 concurrent queries for _ in 0..100 { let jobs_clone = jobs.clone(); let count = query_count.clone(); let times = query_times.clone(); let handle = tokio::spawn(async move { let query_start = Instant::now(); // Simulate indexed query: filter by status let jobs_read = jobs_clone.read().await; let _running_jobs: Vec<_> = jobs_read.values() .filter(|j| j.status == JobStatus::Running) .collect(); let query_elapsed = query_start.elapsed(); count.fetch_add(1, Ordering::Relaxed); let mut times_vec = times.lock().expect("INVARIANT: Lock should not be poisoned"); times_vec.push(query_elapsed); }); handles.push(handle); } // Wait for all queries for handle in handles { handle.await.ok(); } let elapsed = start.elapsed(); let queries = query_count.load(Ordering::Relaxed); let times_vec = query_times.lock().expect("INVARIANT: Lock should not be poisoned"); let mut sorted_times = times_vec.clone(); sorted_times.sort(); let p50 = sorted_times[queries as usize / 2]; let p95 = sorted_times[(queries as usize * 95) / 100]; println!("✓ Test 4 Results:"); println!(" - Duration: {:?}", elapsed); println!(" - Queries executed: {}/100", queries); println!(" - Query P50: {:?}", p50); println!(" - Query P95: {:?}", p95); println!(" - Target P95: <10ms"); assert_eq!(queries, 100, "Expected all 100 queries to complete"); assert!(p95 < Duration::from_millis(10), "P95 {:?} exceeds 10ms target", p95); Ok(()) } /// Test 5: Deadlock Prevention Validation /// /// Tests that concurrent updates to related records don't cause deadlocks. /// Validates lock ordering and transaction isolation. #[tokio::test] #[ignore = "Stress test - run explicitly with --ignored"] async fn test_deadlock_prevention() -> Result<()> { println!("\n=== Test 5: Deadlock Prevention Validation ==="); let start = Instant::now(); let jobs = Arc::new(RwLock::new(std::collections::HashMap::new())); // Create 20 jobs let mut job_ids = Vec::new(); for i in 0..20 { let config = create_test_config(); let job = TrainingJob::new( "DQN".to_string(), config, format!("Deadlock test {}", i), std::collections::HashMap::new(), ); let job_id = job.id; jobs.write().await.insert(job_id, job); job_ids.push(job_id); } let success_count = Arc::new(AtomicU32::new(0)); let deadlock_count = Arc::new(AtomicU32::new(0)); let mut handles = Vec::new(); // Create 100 operations that update pairs of jobs for _ in 0..100 { let jobs_clone = jobs.clone(); let job_ids_clone = job_ids.clone(); let success = success_count.clone(); let deadlocks = deadlock_count.clone(); let handle = tokio::spawn(async move { // Pick two random jobs let idx1 = rand::random::() % job_ids_clone.len(); let idx2 = rand::random::() % job_ids_clone.len(); if idx1 == idx2 { return; } // Always acquire locks in consistent order (by UUID) let (first_id, second_id) = if job_ids_clone[idx1] < job_ids_clone[idx2] { (job_ids_clone[idx1], job_ids_clone[idx2]) } else { (job_ids_clone[idx2], job_ids_clone[idx1]) }; // Acquire lock and update both jobs let operation = async { let mut jobs_write = jobs_clone.write().await; if let Some(job1) = jobs_write.get_mut(&first_id) { job1.progress_percentage += 1.0; } if let Some(job2) = jobs_write.get_mut(&second_id) { job2.progress_percentage += 1.0; } success.fetch_add(1, Ordering::Relaxed); }; // Use timeout to detect potential deadlocks if tokio::time::timeout(Duration::from_secs(5), operation).await.is_err() { deadlocks.fetch_add(1, Ordering::Relaxed); } }); handles.push(handle); } // Wait for all operations for handle in handles { handle.await.ok(); } let elapsed = start.elapsed(); let success = success_count.load(Ordering::Relaxed); let deadlocks = deadlock_count.load(Ordering::Relaxed); println!("✓ Test 5 Results:"); println!(" - Duration: {:?}", elapsed); println!(" - Successful operations: {}", success); println!(" - Detected deadlocks: {}", deadlocks); println!(" - Deadlock-free: {}", deadlocks == 0); assert_eq!(deadlocks, 0, "Detected {} deadlocks - lock ordering failed", deadlocks); assert!(success >= 95, "Expected at least 95 successful operations, got {}", success); Ok(()) }