#![allow( clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_markdown, clippy::empty_line_after_doc_comments, clippy::field_reassign_with_default, clippy::get_unwrap, clippy::identity_op, clippy::inconsistent_digit_grouping, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_underscore_must_use, clippy::manual_div_ceil, clippy::manual_let_else, clippy::manual_range_contains, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::non_ascii_literal, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::too_many_lines, clippy::unnecessary_wraps, clippy::unseparated_literal_suffix, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, clippy::else_if_without_else, clippy::expect_used, clippy::missing_const_for_fn, clippy::similar_names, clippy::type_complexity, clippy::collapsible_else_if, clippy::doc_lazy_continuation, clippy::items_after_test_module, clippy::map_clone, clippy::multiple_unsafe_ops_per_block, clippy::unwrap_or_default, clippy::assign_op_pattern, clippy::needless_borrow, clippy::println_empty_string, clippy::unnecessary_cast, clippy::used_underscore_binding, clippy::create_dir, clippy::implicit_saturating_sub, clippy::exit, clippy::expect_fun_call, clippy::too_many_arguments, clippy::unnecessary_map_or, clippy::unwrap_used, dead_code, unused_imports, unused_variables, clippy::cloned_ref_to_slice_refs, clippy::neg_multiply, clippy::while_let_loop, clippy::bool_assert_comparison, clippy::excessive_precision, clippy::trivially_copy_pass_by_ref, clippy::op_ref, clippy::redundant_closure, clippy::unnecessary_lazy_evaluations, clippy::if_then_some_else_none, clippy::unnecessary_to_owned, clippy::single_component_path_imports, )] //! Integration tests for ensemble checkpoint hot-swapping //! //! Tests the complete hot-swap workflow: //! 1. Load DQN checkpoint into active buffer //! 2. Stage new checkpoint in shadow buffer //! 3. Validate staged checkpoint (1000 predictions, latency checks) //! 4. Commit atomic swap (<1μs) //! 5. Verify active buffer now points to new checkpoint //! 6. Test rollback mechanism use ml::ensemble::{ CheckpointModel, CheckpointValidator, EnsembleMetrics, HotSwapManager, RollbackPolicy, }; use ml::{Features, MLResult, ModelPrediction}; use std::sync::Arc; use std::time::Instant; use tracing::info; /// Mock prediction function for DQN epoch 30 fn create_dqn_epoch_30_predictor( ) -> Arc MLResult + Send + Sync> { Arc::new(|features: &Features| { let value = (features.values.iter().sum::() / features.values.len() as f64) * 0.8; Ok(ModelPrediction::new( "DQN_epoch_30".to_string(), value.tanh(), 0.78, )) }) } /// Mock prediction function for DQN epoch 50 (improved) fn create_dqn_epoch_50_predictor( ) -> Arc MLResult + Send + Sync> { Arc::new(|features: &Features| { let value = (features.values.iter().sum::() / features.values.len() as f64) * 0.9; Ok(ModelPrediction::new( "DQN_epoch_50".to_string(), value.tanh(), 0.85, )) }) } #[tokio::test] async fn test_hot_swap_workflow_complete() { info!("Hot-Swap Workflow Test"); // Initialize hot-swap manager let validator = CheckpointValidator::new(); let policy = RollbackPolicy::default(); let manager = HotSwapManager::new(validator, policy); // Step 1: Load DQN epoch 30 into active buffer info!("Step 1: Loading DQN epoch 30 into active buffer"); let checkpoint_v30 = Arc::new(CheckpointModel::new( "DQN".to_string(), "ml/checkpoints/dqn/checkpoint_epoch_30.safetensors".to_string(), create_dqn_epoch_30_predictor(), )); manager .register_model("DQN".to_string(), checkpoint_v30) .await .expect("Failed to register model"); let active = manager.get_active_checkpoint("DQN").await.unwrap(); assert_eq!( active.checkpoint_path, "ml/checkpoints/dqn/checkpoint_epoch_30.safetensors" ); info!(checkpoint_path = %active.checkpoint_path, "Active checkpoint loaded"); // Step 2: Stage DQN epoch 50 in shadow buffer info!("Step 2: Staging DQN epoch 50 in shadow buffer"); let checkpoint_v50 = Arc::new(CheckpointModel::new( "DQN".to_string(), "ml/checkpoints/dqn/checkpoint_epoch_50.safetensors".to_string(), create_dqn_epoch_50_predictor(), )); manager .stage_checkpoint("DQN", checkpoint_v50) .await .expect("Failed to stage checkpoint"); info!("Staged checkpoint in shadow buffer"); // Step 3: Validate staged checkpoint info!("Step 3: Validating staged checkpoint (1000 predictions)"); let validation_start = Instant::now(); let validation = manager .validate_staged_checkpoint("DQN") .await .expect("Failed to validate checkpoint"); let validation_duration = validation_start.elapsed(); assert!( validation.passed, "Validation failed: {:?}", validation.failure_reason ); assert!( validation.p99_latency_us < 50, "P99 latency {}μs exceeds 50μs", validation.p99_latency_us ); assert!( validation.predictions_in_range >= 950, "Only {} predictions in range", validation.predictions_in_range ); info!("Validation PASSED"); info!(avg_latency_us = validation.avg_latency_us, "Avg latency"); info!(p99_latency_us = validation.p99_latency_us, "P99 latency"); info!( predictions_in_range = validation.predictions_in_range, predictions_validated = validation.predictions_validated, validation_duration_ms = validation_duration.as_millis(), "Validation predictions in range" ); // Record metrics EnsembleMetrics::record_validation( "DQN", true, validation_duration.as_millis() as u64, validation.p99_latency_us, ); // Step 4: Commit atomic swap info!("Step 4: Committing atomic swap"); let swap_latency = manager .commit_swap("DQN") .await .expect("Failed to commit swap"); assert!( swap_latency.as_micros() < 100, "Swap latency {}μs exceeds 100μs", swap_latency.as_micros() ); info!(swap_latency_us = swap_latency.as_micros(), "Atomic swap completed"); // Record metrics EnsembleMetrics::record_swap_success("DQN", swap_latency.as_micros() as u64); // Step 5: Verify active checkpoint is now epoch 50 info!("Step 5: Verifying active checkpoint"); let active = manager.get_active_checkpoint("DQN").await.unwrap(); assert_eq!( active.checkpoint_path, "ml/checkpoints/dqn/checkpoint_epoch_50.safetensors" ); info!(checkpoint_path = %active.checkpoint_path, "Active checkpoint loaded"); // Test prediction with new checkpoint let features = Features::new( vec![0.5, 0.6, 0.7, 0.8, 0.9], vec![ "f1".to_string(), "f2".to_string(), "f3".to_string(), "f4".to_string(), "f5".to_string(), ], ); let prediction = active.predict(&features).unwrap(); assert_eq!(prediction.model_id, "DQN_epoch_50"); assert!(prediction.confidence >= 0.85); info!( value = prediction.value, confidence = prediction.confidence, "Prediction with new checkpoint" ); info!("Hot-Swap Workflow Test PASSED"); } #[tokio::test] async fn test_hot_swap_rollback() { info!("Hot-Swap Rollback Test"); let validator = CheckpointValidator::new(); let policy = RollbackPolicy::default(); let manager = HotSwapManager::new(validator, policy); // Register initial checkpoint let checkpoint_v30 = Arc::new(CheckpointModel::new( "DQN".to_string(), "checkpoint_epoch_30.safetensors".to_string(), create_dqn_epoch_30_predictor(), )); manager .register_model("DQN".to_string(), checkpoint_v30) .await .unwrap(); // Stage and commit new checkpoint let checkpoint_v50 = Arc::new(CheckpointModel::new( "DQN".to_string(), "checkpoint_epoch_50.safetensors".to_string(), create_dqn_epoch_50_predictor(), )); manager .stage_checkpoint("DQN", checkpoint_v50) .await .unwrap(); manager.commit_swap("DQN").await.unwrap(); // Verify new checkpoint is active let active = manager.get_active_checkpoint("DQN").await.unwrap(); assert_eq!(active.checkpoint_path, "checkpoint_epoch_50.safetensors"); info!("Swapped to epoch 50"); // Simulate failure and rollback info!("Simulating failure and rolling back"); manager.rollback("DQN").await.unwrap(); // Verify rollback restored epoch 30 let active = manager.get_active_checkpoint("DQN").await.unwrap(); assert_eq!(active.checkpoint_path, "checkpoint_epoch_30.safetensors"); info!("Rolled back to epoch 30"); // Record metrics EnsembleMetrics::record_swap_rollback("DQN"); EnsembleMetrics::record_rollback("DQN", "test_failure"); info!("Rollback Test PASSED"); } #[tokio::test] async fn test_swap_latency_benchmark() { info!("Swap Latency Benchmark"); let validator = CheckpointValidator::new(); let policy = RollbackPolicy::default(); let manager = HotSwapManager::new(validator, policy); // Register initial checkpoint let checkpoint_v1 = Arc::new(CheckpointModel::new( "DQN".to_string(), "checkpoint_v1.safetensors".to_string(), create_dqn_epoch_30_predictor(), )); manager .register_model("DQN".to_string(), checkpoint_v1) .await .unwrap(); // Run 100 swap operations to benchmark let mut swap_latencies = Vec::new(); for i in 0..100 { let checkpoint = Arc::new(CheckpointModel::new( "DQN".to_string(), format!("checkpoint_v{}.safetensors", i + 2), create_dqn_epoch_50_predictor(), )); manager.stage_checkpoint("DQN", checkpoint).await.unwrap(); let swap_latency = manager.commit_swap("DQN").await.unwrap(); swap_latencies.push(swap_latency.as_micros() as u64); } // Calculate statistics swap_latencies.sort_unstable(); let avg_latency = swap_latencies.iter().sum::() / swap_latencies.len() as u64; let p50_latency = swap_latencies[50]; let p99_latency = swap_latencies[99]; info!("Swap latency statistics (100 swaps)"); info!(avg_latency_us = avg_latency, "Average swap latency"); info!(p50_latency_us = p50_latency, "P50 swap latency"); info!(p99_latency_us = p99_latency, "P99 swap latency"); info!(min_latency_us = swap_latencies[0], "Min swap latency"); info!(max_latency_us = swap_latencies[99], "Max swap latency"); // All swaps should be < 1μs (but we allow 100μs for CI/testing) assert!( p99_latency < 100, "P99 swap latency {}μs exceeds 100μs", p99_latency ); info!("Swap Latency Benchmark PASSED"); } #[tokio::test] async fn test_zero_dropped_predictions() { info!("Zero Dropped Predictions Test"); let validator = CheckpointValidator::new(); let policy = RollbackPolicy::default(); let manager = HotSwapManager::new(validator, policy); // Register initial checkpoint let checkpoint_v30 = Arc::new(CheckpointModel::new( "DQN".to_string(), "checkpoint_epoch_30.safetensors".to_string(), create_dqn_epoch_30_predictor(), )); manager .register_model("DQN".to_string(), checkpoint_v30) .await .unwrap(); // Spawn prediction workload (1000 predictions in background) let manager_clone = Arc::new(manager); let prediction_task = { let manager = manager_clone.clone(); tokio::spawn(async move { let mut success_count = 0; let mut error_count = 0; for i in 0..1000 { let features = Features::new( vec![ (i as f64 * 0.01).sin(), (i as f64 * 0.02).cos(), (i as f64 * 0.03).tanh(), (i as f64 * 0.01 + 1.0).ln(), (i as f64 * 0.001).exp().min(10.0), ], vec![ "f1".to_string(), "f2".to_string(), "f3".to_string(), "f4".to_string(), "f5".to_string(), ], ); match manager.get_active_checkpoint("DQN").await { Ok(checkpoint) => match checkpoint.predict(&features) { Ok(_) => success_count += 1, Err(_) => error_count += 1, }, Err(_) => error_count += 1, } // Small delay to simulate realistic prediction rate tokio::time::sleep(tokio::time::Duration::from_micros(100)).await; } (success_count, error_count) }) }; // Wait a bit for predictions to start tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; // Perform hot-swap while predictions are running info!("Performing hot-swap during active predictions"); let checkpoint_v50 = Arc::new(CheckpointModel::new( "DQN".to_string(), "checkpoint_epoch_50.safetensors".to_string(), create_dqn_epoch_50_predictor(), )); manager_clone .stage_checkpoint("DQN", checkpoint_v50) .await .unwrap(); let swap_latency = manager_clone.commit_swap("DQN").await.unwrap(); info!(swap_latency_us = swap_latency.as_micros(), "Hot-swap completed during active predictions"); // Wait for prediction task to complete let (success_count, error_count) = prediction_task.await.unwrap(); info!("Prediction results during hot-swap"); info!(success_count, "Successful predictions"); info!(error_count, "Error predictions"); info!(total = success_count + error_count, "Total predictions"); // Verify zero dropped predictions (all predictions should succeed) assert_eq!( error_count, 0, "Found {} dropped predictions during hot-swap", error_count ); assert_eq!( success_count, 1000, "Expected 1000 successful predictions, got {}", success_count ); info!("Zero Dropped Predictions Test PASSED"); }