Reduce log noise for non-critical operational paths: connection retries, expected fallbacks, graceful degradation, and optional feature absence. Keeps warn/error for genuine failures requiring attention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
708 lines
23 KiB
Rust
708 lines
23 KiB
Rust
//! Automated Production Deployment Pipeline for Trained ML Models
|
|
//!
|
|
//! **Architecture**:
|
|
//! 1. Training completes → Validation passes
|
|
//! 2. A/B test passes → Promote to production
|
|
//! 3. Rolling update (zero downtime)
|
|
//! 4. Health check (model serving correctly)
|
|
//! 5. Rollback capability (if health check fails)
|
|
//!
|
|
//! **Zero Downtime Strategy**:
|
|
//! - Rolling update: Update instances in batches
|
|
//! - Health check: Verify model inference before routing traffic
|
|
//! - Rollback: Automatic revert to previous model on failure
|
|
|
|
use anyhow::Result;
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::sync::RwLock;
|
|
use tokio::time::sleep;
|
|
use tracing::{debug, error, info, warn};
|
|
use uuid::Uuid;
|
|
|
|
// ==================== CONFIGURATION ====================
|
|
|
|
/// Deployment pipeline configuration
|
|
#[derive(Debug, Clone)]
|
|
pub struct DeploymentConfig {
|
|
/// Enable automatic deployment
|
|
pub enable_auto_deployment: bool,
|
|
/// Trigger deployment on A/B test pass
|
|
pub trigger_on_ab_test_pass: bool,
|
|
/// Minimum A/B test confidence (0.0 - 1.0)
|
|
pub min_ab_test_confidence: f64,
|
|
/// Rolling update configuration
|
|
pub rolling_update: RollingUpdateConfig,
|
|
/// Health check configuration
|
|
pub health_check: HealthCheckConfig,
|
|
/// Rollback strategy
|
|
pub rollback_strategy: RollbackStrategy,
|
|
/// Rollback on health check failure
|
|
pub rollback_on_health_check_failure: bool,
|
|
}
|
|
|
|
impl Default for DeploymentConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enable_auto_deployment: true,
|
|
trigger_on_ab_test_pass: true,
|
|
min_ab_test_confidence: 0.95,
|
|
rolling_update: RollingUpdateConfig::default(),
|
|
health_check: HealthCheckConfig::default(),
|
|
rollback_strategy: RollbackStrategy::Automatic,
|
|
rollback_on_health_check_failure: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rolling update configuration
|
|
#[derive(Debug, Clone)]
|
|
pub struct RollingUpdateConfig {
|
|
/// Number of instances to update at once
|
|
pub batch_size: usize,
|
|
/// Delay between batches (seconds)
|
|
pub batch_delay_seconds: u64,
|
|
/// Health check retries per instance
|
|
pub health_check_retries: u32,
|
|
/// Health check interval (seconds)
|
|
pub health_check_interval_seconds: u64,
|
|
}
|
|
|
|
impl Default for RollingUpdateConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
batch_size: 1,
|
|
batch_delay_seconds: 5,
|
|
health_check_retries: 3,
|
|
health_check_interval_seconds: 2,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Health check configuration
|
|
#[derive(Debug, Clone)]
|
|
pub struct HealthCheckConfig {
|
|
/// Enable health checks
|
|
pub enabled: bool,
|
|
/// Health check timeout (seconds)
|
|
pub timeout_seconds: u64,
|
|
/// Maximum allowed latency (milliseconds)
|
|
pub max_latency_ms: u64,
|
|
/// Number of test predictions to run
|
|
pub test_predictions: usize,
|
|
/// Minimum success rate (0.0 - 1.0)
|
|
pub min_success_rate: f64,
|
|
}
|
|
|
|
impl Default for HealthCheckConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
timeout_seconds: 10,
|
|
max_latency_ms: 100,
|
|
test_predictions: 10,
|
|
min_success_rate: 0.95,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rollback strategy
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum RollbackStrategy {
|
|
/// Automatic rollback on failure
|
|
Automatic,
|
|
/// Manual rollback (requires operator intervention)
|
|
Manual,
|
|
}
|
|
|
|
// ==================== RESULT TYPES ====================
|
|
|
|
/// Deployment status
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum DeploymentStatus {
|
|
/// Deployment triggered
|
|
Triggered,
|
|
/// Deployment in progress
|
|
InProgress,
|
|
/// Deployment completed successfully
|
|
Completed,
|
|
/// Deployment failed
|
|
Failed,
|
|
/// Deployment skipped (e.g., A/B test did not pass)
|
|
Skipped,
|
|
/// Deployment rolled back
|
|
RolledBack,
|
|
}
|
|
|
|
/// Deployment result
|
|
#[derive(Debug, Clone)]
|
|
pub struct DeploymentResult {
|
|
/// Deployment ID
|
|
pub deployment_id: Uuid,
|
|
/// Model ID being deployed
|
|
pub model_id: Uuid,
|
|
/// Deployment status
|
|
pub status: DeploymentStatus,
|
|
/// Number of instances updated
|
|
pub instances_updated: usize,
|
|
/// Number of batches executed
|
|
pub batches_executed: usize,
|
|
/// Zero downtime achieved
|
|
pub zero_downtime_achieved: bool,
|
|
/// Deployment duration (seconds)
|
|
pub deployment_duration_seconds: u64,
|
|
/// Triggered by A/B test
|
|
pub triggered_by_ab_test: bool,
|
|
/// Rollback triggered
|
|
pub rollback_triggered: bool,
|
|
/// Active model ID after deployment
|
|
pub active_model_id: Uuid,
|
|
/// Updated instance IDs
|
|
pub updated_instances: Vec<String>,
|
|
/// Deployment timestamp
|
|
pub deployed_at: DateTime<Utc>,
|
|
/// Error message (if failed)
|
|
pub error_message: Option<String>,
|
|
}
|
|
|
|
/// Health check result
|
|
#[derive(Debug, Clone)]
|
|
pub struct HealthCheckResult {
|
|
/// Instance ID
|
|
pub instance_id: String,
|
|
/// Health status
|
|
pub healthy: bool,
|
|
/// Model inference working
|
|
pub inference_working: bool,
|
|
/// Average latency (milliseconds)
|
|
pub latency_ms: f64,
|
|
/// Success rate (0.0 - 1.0)
|
|
pub success_rate: f64,
|
|
/// Error message (if unhealthy)
|
|
pub error_message: Option<String>,
|
|
}
|
|
|
|
/// Rollback result
|
|
#[derive(Debug, Clone)]
|
|
pub struct RollbackResult {
|
|
/// Rollback successful
|
|
pub rollback_successful: bool,
|
|
/// Active model ID after rollback
|
|
pub active_model_id: Uuid,
|
|
/// Rollback duration (seconds)
|
|
pub rollback_duration_seconds: u64,
|
|
/// Rollback timestamp
|
|
pub rolled_back_at: DateTime<Utc>,
|
|
}
|
|
|
|
/// Deployment status info
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DeploymentStatusInfo {
|
|
/// Deployment ID
|
|
pub deployment_id: Uuid,
|
|
/// Model ID
|
|
pub model_id: Uuid,
|
|
/// Status
|
|
pub status: DeploymentStatus,
|
|
/// Progress (0.0 - 1.0)
|
|
pub progress: f64,
|
|
/// Instances updated
|
|
pub instances_updated: usize,
|
|
/// Total instances
|
|
pub total_instances: usize,
|
|
/// Started at
|
|
pub started_at: DateTime<Utc>,
|
|
}
|
|
|
|
// ==================== DEPLOYMENT PIPELINE ====================
|
|
|
|
/// Automated production deployment pipeline
|
|
pub struct DeploymentPipeline {
|
|
/// Configuration
|
|
config: DeploymentConfig,
|
|
/// Active deployments
|
|
active_deployments: Arc<RwLock<HashMap<Uuid, DeploymentState>>>,
|
|
/// Deployment history
|
|
deployment_history: Arc<RwLock<Vec<DeploymentResult>>>,
|
|
/// Current model per instance
|
|
instance_models: Arc<RwLock<HashMap<String, Uuid>>>,
|
|
}
|
|
|
|
/// Internal deployment state
|
|
#[derive(Debug, Clone)]
|
|
struct DeploymentState {
|
|
deployment_id: Uuid,
|
|
model_id: Uuid,
|
|
status: DeploymentStatus,
|
|
started_at: DateTime<Utc>,
|
|
instances_updated: usize,
|
|
total_instances: usize,
|
|
}
|
|
|
|
impl DeploymentPipeline {
|
|
/// Create new deployment pipeline
|
|
pub fn new(config: DeploymentConfig) -> Result<Self> {
|
|
info!("Initializing deployment pipeline");
|
|
|
|
Ok(Self {
|
|
config,
|
|
active_deployments: Arc::new(RwLock::new(HashMap::new())),
|
|
deployment_history: Arc::new(RwLock::new(Vec::new())),
|
|
instance_models: Arc::new(RwLock::new(HashMap::new())),
|
|
})
|
|
}
|
|
|
|
/// Trigger deployment on A/B test completion
|
|
pub async fn trigger_deployment_on_ab_test(
|
|
&self,
|
|
ab_test_result: ABTestResult,
|
|
) -> Result<DeploymentResult> {
|
|
info!(
|
|
"Evaluating A/B test result for deployment trigger (model: {})",
|
|
ab_test_result.model_id
|
|
);
|
|
|
|
// Check if A/B test passed
|
|
if !ab_test_result.passed {
|
|
warn!("A/B test did not pass thresholds, skipping deployment");
|
|
return Ok(DeploymentResult {
|
|
deployment_id: Uuid::new_v4(),
|
|
model_id: ab_test_result.model_id,
|
|
status: DeploymentStatus::Skipped,
|
|
instances_updated: 0,
|
|
batches_executed: 0,
|
|
zero_downtime_achieved: false,
|
|
deployment_duration_seconds: 0,
|
|
triggered_by_ab_test: false,
|
|
rollback_triggered: false,
|
|
active_model_id: ab_test_result.model_id,
|
|
updated_instances: Vec::new(),
|
|
deployed_at: Utc::now(),
|
|
error_message: Some("A/B test failed thresholds".to_string()),
|
|
});
|
|
}
|
|
|
|
// Check confidence threshold
|
|
if ab_test_result.statistical_significance < self.config.min_ab_test_confidence {
|
|
warn!(
|
|
"A/B test confidence ({:.2}) below threshold ({:.2}), skipping deployment",
|
|
ab_test_result.statistical_significance, self.config.min_ab_test_confidence
|
|
);
|
|
return Ok(DeploymentResult {
|
|
deployment_id: Uuid::new_v4(),
|
|
model_id: ab_test_result.model_id,
|
|
status: DeploymentStatus::Skipped,
|
|
instances_updated: 0,
|
|
batches_executed: 0,
|
|
zero_downtime_achieved: false,
|
|
deployment_duration_seconds: 0,
|
|
triggered_by_ab_test: false,
|
|
rollback_triggered: false,
|
|
active_model_id: ab_test_result.model_id,
|
|
updated_instances: Vec::new(),
|
|
deployed_at: Utc::now(),
|
|
error_message: Some(format!(
|
|
"Confidence {:.2} below threshold {:.2}",
|
|
ab_test_result.statistical_significance, self.config.min_ab_test_confidence
|
|
)),
|
|
});
|
|
}
|
|
|
|
// Trigger deployment
|
|
info!(
|
|
"✅ A/B test passed (confidence: {:.2}%), triggering deployment",
|
|
ab_test_result.statistical_significance * 100.0
|
|
);
|
|
|
|
Ok(DeploymentResult {
|
|
deployment_id: Uuid::new_v4(),
|
|
model_id: ab_test_result.model_id,
|
|
status: DeploymentStatus::Triggered,
|
|
instances_updated: 0,
|
|
batches_executed: 0,
|
|
zero_downtime_achieved: false,
|
|
deployment_duration_seconds: 0,
|
|
triggered_by_ab_test: true,
|
|
rollback_triggered: false,
|
|
active_model_id: ab_test_result.model_id,
|
|
updated_instances: Vec::new(),
|
|
deployed_at: Utc::now(),
|
|
error_message: None,
|
|
})
|
|
}
|
|
|
|
/// Perform rolling update with zero downtime
|
|
pub async fn perform_rolling_update(
|
|
&self,
|
|
model_id: Uuid,
|
|
model_path: &str,
|
|
total_instances: usize,
|
|
) -> Result<DeploymentResult> {
|
|
let deployment_id = Uuid::new_v4();
|
|
let start_time = std::time::Instant::now();
|
|
|
|
info!(
|
|
"🚀 Starting rolling update: deployment_id={}, model_id={}, instances={}",
|
|
deployment_id, model_id, total_instances
|
|
);
|
|
|
|
// Register deployment
|
|
self.start_deployment(deployment_id, model_id).await?;
|
|
|
|
let batch_size = self.config.rolling_update.batch_size;
|
|
let num_batches = total_instances.div_ceil(batch_size);
|
|
let mut updated_instances = Vec::new();
|
|
|
|
// Process instances in batches
|
|
for batch_idx in 0..num_batches {
|
|
let start_idx = batch_idx * batch_size;
|
|
let end_idx = ((batch_idx + 1) * batch_size).min(total_instances);
|
|
let batch_instances: Vec<String> = (start_idx..end_idx)
|
|
.map(|i| format!("trading-service-{}", i + 1))
|
|
.collect();
|
|
|
|
info!(
|
|
"📦 Processing batch {}/{}: {} instances",
|
|
batch_idx + 1,
|
|
num_batches,
|
|
batch_instances.len()
|
|
);
|
|
|
|
// Update instances in this batch
|
|
for instance_id in &batch_instances {
|
|
debug!("Updating instance: {}", instance_id);
|
|
|
|
// Simulate model loading (in production: gRPC call to TradingService)
|
|
self.load_model_on_instance(instance_id, model_id, model_path)
|
|
.await?;
|
|
|
|
// Run health check
|
|
if self.config.health_check.enabled {
|
|
let health = self.run_health_check(model_id, instance_id).await?;
|
|
if !health.healthy {
|
|
error!(
|
|
"Health check failed for instance {}: {:?}",
|
|
instance_id, health
|
|
);
|
|
return Ok(DeploymentResult {
|
|
deployment_id,
|
|
model_id,
|
|
status: DeploymentStatus::Failed,
|
|
instances_updated: updated_instances.len(),
|
|
batches_executed: batch_idx + 1,
|
|
zero_downtime_achieved: false,
|
|
deployment_duration_seconds: start_time.elapsed().as_secs(),
|
|
triggered_by_ab_test: false,
|
|
rollback_triggered: false,
|
|
active_model_id: model_id,
|
|
updated_instances,
|
|
deployed_at: Utc::now(),
|
|
error_message: Some(format!(
|
|
"Health check failed for instance {}",
|
|
instance_id
|
|
)),
|
|
});
|
|
}
|
|
}
|
|
|
|
updated_instances.push(instance_id.clone());
|
|
debug!("✅ Instance {} updated successfully", instance_id);
|
|
}
|
|
|
|
// Delay between batches (except for last batch)
|
|
if batch_idx < num_batches - 1 {
|
|
debug!(
|
|
"⏳ Waiting {} seconds before next batch",
|
|
self.config.rolling_update.batch_delay_seconds
|
|
);
|
|
sleep(Duration::from_secs(
|
|
self.config.rolling_update.batch_delay_seconds,
|
|
))
|
|
.await;
|
|
}
|
|
}
|
|
|
|
let duration = start_time.elapsed();
|
|
info!(
|
|
"✅ Rolling update completed: {} instances updated in {:.2}s",
|
|
updated_instances.len(),
|
|
duration.as_secs_f64()
|
|
);
|
|
|
|
Ok(DeploymentResult {
|
|
deployment_id,
|
|
model_id,
|
|
status: DeploymentStatus::Completed,
|
|
instances_updated: updated_instances.len(),
|
|
batches_executed: num_batches,
|
|
zero_downtime_achieved: true,
|
|
deployment_duration_seconds: duration.as_secs(),
|
|
triggered_by_ab_test: false,
|
|
rollback_triggered: false,
|
|
active_model_id: model_id,
|
|
updated_instances,
|
|
deployed_at: Utc::now(),
|
|
error_message: None,
|
|
})
|
|
}
|
|
|
|
/// Deploy with automatic rollback on failure
|
|
pub async fn deploy_with_rollback(
|
|
&self,
|
|
model_id: Uuid,
|
|
previous_model_id: Uuid,
|
|
model_path: &str,
|
|
total_instances: usize,
|
|
) -> Result<DeploymentResult> {
|
|
info!(
|
|
"Deploying model {} with rollback capability (previous: {})",
|
|
model_id, previous_model_id
|
|
);
|
|
|
|
// Attempt deployment
|
|
let deployment_result = self
|
|
.perform_rolling_update(model_id, model_path, total_instances)
|
|
.await?;
|
|
|
|
// Check if deployment failed
|
|
if deployment_result.status == DeploymentStatus::Failed
|
|
&& self.config.rollback_on_health_check_failure
|
|
&& self.config.rollback_strategy == RollbackStrategy::Automatic
|
|
{
|
|
error!("Deployment failed, triggering automatic rollback");
|
|
|
|
// Perform rollback
|
|
let rollback_result = self
|
|
.rollback_deployment(model_id, previous_model_id)
|
|
.await?;
|
|
|
|
return Ok(DeploymentResult {
|
|
rollback_triggered: true,
|
|
status: DeploymentStatus::RolledBack,
|
|
active_model_id: rollback_result.active_model_id,
|
|
..deployment_result
|
|
});
|
|
}
|
|
|
|
Ok(deployment_result)
|
|
}
|
|
|
|
/// Run health check on deployed model
|
|
pub async fn run_health_check(
|
|
&self,
|
|
model_id: Uuid,
|
|
instance_id: &str,
|
|
) -> Result<HealthCheckResult> {
|
|
debug!(
|
|
"Running health check: model={}, instance={}",
|
|
model_id, instance_id
|
|
);
|
|
|
|
// Simulate health check based on instance name
|
|
let is_broken = instance_id.contains("broken");
|
|
let is_slow = instance_id.contains("slow");
|
|
|
|
if is_broken {
|
|
// Simulate broken instance
|
|
return Ok(HealthCheckResult {
|
|
instance_id: instance_id.to_string(),
|
|
healthy: false,
|
|
inference_working: false,
|
|
latency_ms: 0.0,
|
|
success_rate: 0.0,
|
|
error_message: Some("Model inference failed".to_string()),
|
|
});
|
|
}
|
|
|
|
if is_slow {
|
|
// Simulate slow instance
|
|
return Ok(HealthCheckResult {
|
|
instance_id: instance_id.to_string(),
|
|
healthy: false,
|
|
inference_working: true,
|
|
latency_ms: 500.0, // High latency
|
|
success_rate: 1.0,
|
|
error_message: Some(format!(
|
|
"Latency {:.2}ms exceeds threshold {}ms",
|
|
500.0, self.config.health_check.max_latency_ms
|
|
)),
|
|
});
|
|
}
|
|
|
|
// Simulate successful health check
|
|
let latency_ms = 45.0;
|
|
let success_rate = 0.98;
|
|
|
|
Ok(HealthCheckResult {
|
|
instance_id: instance_id.to_string(),
|
|
healthy: latency_ms <= self.config.health_check.max_latency_ms as f64
|
|
&& success_rate >= self.config.health_check.min_success_rate,
|
|
inference_working: true,
|
|
latency_ms,
|
|
success_rate,
|
|
error_message: None,
|
|
})
|
|
}
|
|
|
|
/// Rollback to previous model
|
|
pub async fn rollback_deployment(
|
|
&self,
|
|
_current_model_id: Uuid,
|
|
previous_model_id: Uuid,
|
|
) -> Result<RollbackResult> {
|
|
let start_time = std::time::Instant::now();
|
|
|
|
info!("🔄 Rolling back to previous model: {}", previous_model_id);
|
|
|
|
// In production: Issue rollback commands to all TradingService instances
|
|
// For now: Simulate fast rollback
|
|
sleep(Duration::from_millis(500)).await;
|
|
|
|
let duration = start_time.elapsed();
|
|
|
|
info!("✅ Rollback completed in {:.2}s", duration.as_secs_f64());
|
|
|
|
Ok(RollbackResult {
|
|
rollback_successful: true,
|
|
active_model_id: previous_model_id,
|
|
rollback_duration_seconds: duration.as_secs(),
|
|
rolled_back_at: Utc::now(),
|
|
})
|
|
}
|
|
|
|
/// Start a deployment (internal tracking)
|
|
pub async fn start_deployment(&self, deployment_id: Uuid, model_id: Uuid) -> Result<()> {
|
|
let mut active = self.active_deployments.write().await;
|
|
|
|
// Check for concurrent deployments
|
|
if !active.is_empty() {
|
|
return Err(anyhow::anyhow!("Deployment already in progress"));
|
|
}
|
|
|
|
active.insert(
|
|
deployment_id,
|
|
DeploymentState {
|
|
deployment_id,
|
|
model_id,
|
|
status: DeploymentStatus::InProgress,
|
|
started_at: Utc::now(),
|
|
instances_updated: 0,
|
|
total_instances: 0,
|
|
},
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get deployment status
|
|
pub async fn get_deployment_status(&self, deployment_id: Uuid) -> Result<DeploymentStatusInfo> {
|
|
let active = self.active_deployments.read().await;
|
|
|
|
let state = active
|
|
.get(&deployment_id)
|
|
.ok_or_else(|| anyhow::anyhow!("Deployment not found"))?;
|
|
|
|
Ok(DeploymentStatusInfo {
|
|
deployment_id: state.deployment_id,
|
|
model_id: state.model_id,
|
|
status: state.status.clone(),
|
|
progress: if state.total_instances > 0 {
|
|
state.instances_updated as f64 / state.total_instances as f64
|
|
} else {
|
|
0.0
|
|
},
|
|
instances_updated: state.instances_updated,
|
|
total_instances: state.total_instances,
|
|
started_at: state.started_at,
|
|
})
|
|
}
|
|
|
|
/// Get deployment history
|
|
pub async fn get_deployment_history(&self, limit: usize) -> Result<Vec<DeploymentResult>> {
|
|
let history = self.deployment_history.read().await;
|
|
Ok(history.iter().rev().take(limit).cloned().collect())
|
|
}
|
|
|
|
/// Load model on instance (simulate gRPC call)
|
|
async fn load_model_on_instance(
|
|
&self,
|
|
instance_id: &str,
|
|
model_id: Uuid,
|
|
_model_path: &str,
|
|
) -> Result<()> {
|
|
debug!("Loading model {} on instance {}", model_id, instance_id);
|
|
|
|
// Simulate model loading delay
|
|
sleep(Duration::from_millis(100)).await;
|
|
|
|
// Update instance model mapping
|
|
let mut instance_models = self.instance_models.write().await;
|
|
instance_models.insert(instance_id.to_string(), model_id);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// ==================== EXTERNAL DATA STRUCTURES ====================
|
|
|
|
/// A/B test result (matches ml/src/deployment/ab_testing.rs)
|
|
#[derive(Debug, Clone)]
|
|
pub struct ABTestResult {
|
|
pub experiment_id: Uuid,
|
|
pub model_id: Uuid,
|
|
pub control_metrics: GroupMetrics,
|
|
pub treatment_metrics: GroupMetrics,
|
|
pub statistical_significance: f64,
|
|
pub p_value: f64,
|
|
pub passed: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct GroupMetrics {
|
|
pub avg_latency_ms: f64,
|
|
pub error_rate: f64,
|
|
pub sharpe_ratio: f64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_deployment_config_default() {
|
|
let config = DeploymentConfig::default();
|
|
assert!(config.enable_auto_deployment);
|
|
assert_eq!(config.min_ab_test_confidence, 0.95);
|
|
assert_eq!(config.rolling_update.batch_size, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_pipeline_creation() {
|
|
let config = DeploymentConfig::default();
|
|
let pipeline = DeploymentPipeline::new(config);
|
|
assert!(pipeline.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_deployment_prevention() {
|
|
let config = DeploymentConfig::default();
|
|
let pipeline = DeploymentPipeline::new(config).unwrap();
|
|
|
|
let deployment_id_1 = Uuid::new_v4();
|
|
let deployment_id_2 = Uuid::new_v4();
|
|
let model_id = Uuid::new_v4();
|
|
|
|
// Start first deployment
|
|
let result1 = pipeline.start_deployment(deployment_id_1, model_id).await;
|
|
assert!(result1.is_ok());
|
|
|
|
// Try to start second deployment (should fail)
|
|
let result2 = pipeline.start_deployment(deployment_id_2, model_id).await;
|
|
assert!(result2.is_err());
|
|
}
|
|
}
|