Skip to main content

trustformers_core/versioning/
deployment.rs

1//! Model deployment management for production environments
2
3use anyhow::Result;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9use uuid::Uuid;
10
11use super::metadata::VersionedModel;
12use super::storage::ModelStorage;
13use crate::ab_testing::{ABTestManager, ExperimentConfig, Variant};
14
15/// Deployment manager for model versions
16pub struct DeploymentManager {
17    #[allow(dead_code)] // Reserved for future persistent storage integration
18    storage: Arc<dyn ModelStorage>,
19    deployments: RwLock<HashMap<String, ActiveDeployment>>,
20    deployment_history: RwLock<HashMap<String, Vec<DeploymentEvent>>>,
21    ab_test_manager: Arc<ABTestManager>,
22}
23
24impl DeploymentManager {
25    /// Create a new deployment manager
26    pub fn new(storage: Arc<dyn ModelStorage>) -> Self {
27        Self {
28            storage,
29            deployments: RwLock::new(HashMap::new()),
30            deployment_history: RwLock::new(HashMap::new()),
31            ab_test_manager: Arc::new(ABTestManager::new()),
32        }
33    }
34
35    /// Deploy a model version to production
36    pub async fn deploy_to_production(
37        &self,
38        version_id: Uuid,
39        model: &VersionedModel,
40    ) -> Result<String> {
41        let deployment_id = format!("{}:production", model.model_name());
42
43        let deployment = ActiveDeployment {
44            deployment_id: deployment_id.clone(),
45            model_name: model.model_name().to_string(),
46            version_id,
47            environment: Environment::Production,
48            strategy: DeploymentStrategy::BlueGreen,
49            status: DeploymentStatus::Deploying,
50            traffic_percentage: 100.0,
51            deployment_time: Utc::now(),
52            health_check_url: None,
53            rollback_version: None,
54            config_overrides: HashMap::new(),
55        };
56
57        // Record deployment event
58        self.record_deployment_event(
59            &deployment_id,
60            DeploymentEvent {
61                event_type: DeploymentEventType::Deploy,
62                version_id,
63                timestamp: Utc::now(),
64                message: format!(
65                    "Deploying {}:{} to production",
66                    model.model_name(),
67                    model.version()
68                ),
69                triggered_by: "system".to_string(),
70                metadata: HashMap::new(),
71            },
72        )
73        .await;
74
75        // Update active deployments
76        {
77            let mut deployments = self.deployments.write().await;
78            deployments.insert(deployment_id.clone(), deployment);
79        }
80
81        // Mark as active after deployment completes
82        tokio::time::sleep(std::time::Duration::from_secs(1)).await; // Simulate deployment time
83        self.mark_deployment_active(&deployment_id).await?;
84
85        tracing::info!(
86            "Deployed {}:{} to production",
87            model.model_name(),
88            model.version()
89        );
90        Ok(deployment_id)
91    }
92
93    /// Deploy using specific strategy
94    pub async fn deploy_with_strategy(
95        &self,
96        version_id: Uuid,
97        model: &VersionedModel,
98        config: DeploymentConfig,
99    ) -> Result<String> {
100        let _deployment_id = format!("{}:{}", model.model_name(), config.environment);
101
102        match config.strategy {
103            DeploymentStrategy::Canary => self.deploy_canary(version_id, model, config).await,
104            DeploymentStrategy::BlueGreen => {
105                self.deploy_blue_green(version_id, model, config).await
106            },
107            DeploymentStrategy::RollingUpdate => {
108                self.deploy_rolling_update(version_id, model, config).await
109            },
110            DeploymentStrategy::ABTest => self.deploy_ab_test(version_id, model, config).await,
111        }
112    }
113
114    /// Rollback to a previous version
115    pub async fn rollback(&self, model_name: &str, target_version_id: Uuid) -> Result<()> {
116        let deployment_id = format!("{}:production", model_name);
117
118        // Get current deployment
119        let current_deployment = {
120            let deployments = self.deployments.read().await;
121            deployments.get(&deployment_id).cloned()
122        };
123
124        if let Some(mut deployment) = current_deployment {
125            let previous_version = deployment.version_id;
126            deployment.rollback_version = Some(previous_version);
127            deployment.version_id = target_version_id;
128            deployment.status = DeploymentStatus::RollingBack;
129
130            // Update deployment
131            {
132                let mut deployments = self.deployments.write().await;
133                deployments.insert(deployment_id.clone(), deployment);
134            }
135
136            // Record rollback event
137            self.record_deployment_event(
138                &deployment_id,
139                DeploymentEvent {
140                    event_type: DeploymentEventType::Rollback,
141                    version_id: target_version_id,
142                    timestamp: Utc::now(),
143                    message: format!(
144                        "Rolling back {} from {} to {}",
145                        model_name, previous_version, target_version_id
146                    ),
147                    triggered_by: "user".to_string(),
148                    metadata: HashMap::new(),
149                },
150            )
151            .await;
152
153            // Simulate rollback process
154            tokio::time::sleep(std::time::Duration::from_secs(2)).await;
155            self.mark_deployment_active(&deployment_id).await?;
156
157            tracing::info!(
158                "Rolled back {} to version {}",
159                model_name,
160                target_version_id
161            );
162            Ok(())
163        } else {
164            anyhow::bail!("No active deployment found for model {}", model_name);
165        }
166    }
167
168    /// Get active deployment for a model
169    pub async fn get_active_deployment(
170        &self,
171        model_name: &str,
172    ) -> Result<Option<ActiveDeployment>> {
173        let deployment_id = format!("{}:production", model_name);
174        let deployments = self.deployments.read().await;
175        Ok(deployments.get(&deployment_id).cloned())
176    }
177
178    /// List all active deployments
179    pub async fn list_deployments(&self) -> Result<Vec<ActiveDeployment>> {
180        let deployments = self.deployments.read().await;
181        Ok(deployments.values().cloned().collect())
182    }
183
184    /// Get deployment history
185    pub async fn get_deployment_history(&self, model_name: &str) -> Result<Vec<DeploymentEvent>> {
186        let deployment_id = format!("{}:production", model_name);
187        let history = self.deployment_history.read().await;
188        Ok(history.get(&deployment_id).cloned().unwrap_or_default())
189    }
190
191    /// Update traffic percentage for gradual rollouts
192    pub async fn update_traffic_percentage(
193        &self,
194        deployment_id: &str,
195        percentage: f64,
196    ) -> Result<()> {
197        if !(0.0..=100.0).contains(&percentage) {
198            anyhow::bail!("Traffic percentage must be between 0 and 100");
199        }
200
201        let mut deployments = self.deployments.write().await;
202        if let Some(deployment) = deployments.get_mut(deployment_id) {
203            deployment.traffic_percentage = percentage;
204
205            self.record_deployment_event(
206                deployment_id,
207                DeploymentEvent {
208                    event_type: DeploymentEventType::TrafficUpdate,
209                    version_id: deployment.version_id,
210                    timestamp: Utc::now(),
211                    message: format!("Updated traffic to {:.1}%", percentage),
212                    triggered_by: "user".to_string(),
213                    metadata: [("traffic_percentage".to_string(), percentage.into())].into(),
214                },
215            )
216            .await;
217
218            tracing::info!(
219                "Updated traffic for {} to {:.1}%",
220                deployment_id,
221                percentage
222            );
223            Ok(())
224        } else {
225            anyhow::bail!("Deployment {} not found", deployment_id);
226        }
227    }
228
229    /// Health check for a deployment
230    pub async fn health_check(&self, deployment_id: &str) -> Result<HealthStatus> {
231        let deployments = self.deployments.read().await;
232        if let Some(deployment) = deployments.get(deployment_id) {
233            // Simulate health check
234            let is_healthy = deployment.status == DeploymentStatus::Active;
235
236            Ok(HealthStatus {
237                deployment_id: deployment_id.to_string(),
238                is_healthy,
239                last_check: Utc::now(),
240                response_time_ms: 120,
241                error_rate_percent: if is_healthy { 0.1 } else { 5.0 },
242                metrics: HashMap::new(),
243            })
244        } else {
245            anyhow::bail!("Deployment {} not found", deployment_id);
246        }
247    }
248
249    /// Get deployment statistics
250    pub async fn get_deployment_stats(&self) -> Result<DeploymentStatistics> {
251        let deployments = self.deployments.read().await;
252
253        let mut stats = DeploymentStatistics {
254            total_deployments: deployments.len(),
255            active_deployments: 0,
256            failed_deployments: 0,
257            deploying_count: 0,
258            rolling_back_count: 0,
259            environments: HashMap::new(),
260        };
261
262        for deployment in deployments.values() {
263            match deployment.status {
264                DeploymentStatus::Active => stats.active_deployments += 1,
265                DeploymentStatus::Failed => stats.failed_deployments += 1,
266                DeploymentStatus::Deploying => stats.deploying_count += 1,
267                DeploymentStatus::RollingBack => stats.rolling_back_count += 1,
268                _ => {},
269            }
270
271            let env_name = deployment.environment.to_string();
272            *stats.environments.entry(env_name).or_insert(0) += 1;
273        }
274
275        Ok(stats)
276    }
277
278    // Private helper methods
279
280    async fn deploy_canary(
281        &self,
282        version_id: Uuid,
283        model: &VersionedModel,
284        config: DeploymentConfig,
285    ) -> Result<String> {
286        let deployment_id = format!("{}:canary", model.model_name());
287
288        let deployment = ActiveDeployment {
289            deployment_id: deployment_id.clone(),
290            model_name: model.model_name().to_string(),
291            version_id,
292            environment: config.environment,
293            strategy: DeploymentStrategy::Canary,
294            status: DeploymentStatus::Deploying,
295            traffic_percentage: config.initial_traffic_percentage.unwrap_or(5.0),
296            deployment_time: Utc::now(),
297            health_check_url: config.health_check_url,
298            rollback_version: None,
299            config_overrides: config.config_overrides,
300        };
301
302        {
303            let mut deployments = self.deployments.write().await;
304            deployments.insert(deployment_id.clone(), deployment);
305        }
306
307        // Start with low traffic
308        self.mark_deployment_active(&deployment_id).await?;
309
310        tracing::info!(
311            "Started canary deployment for {}:{}",
312            model.model_name(),
313            model.version()
314        );
315        Ok(deployment_id)
316    }
317
318    async fn deploy_blue_green(
319        &self,
320        version_id: Uuid,
321        model: &VersionedModel,
322        config: DeploymentConfig,
323    ) -> Result<String> {
324        let deployment_id = format!("{}:blue-green", model.model_name());
325
326        // Deploy to "blue" environment first
327        let deployment = ActiveDeployment {
328            deployment_id: deployment_id.clone(),
329            model_name: model.model_name().to_string(),
330            version_id,
331            environment: config.environment,
332            strategy: DeploymentStrategy::BlueGreen,
333            status: DeploymentStatus::Deploying,
334            traffic_percentage: 0.0, // No traffic initially
335            deployment_time: Utc::now(),
336            health_check_url: config.health_check_url,
337            rollback_version: None,
338            config_overrides: config.config_overrides,
339        };
340
341        {
342            let mut deployments = self.deployments.write().await;
343            deployments.insert(deployment_id.clone(), deployment);
344        }
345
346        // Health check before switching traffic
347        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
348        let health = self.health_check(&deployment_id).await?;
349
350        if health.is_healthy {
351            // Switch traffic to blue environment
352            self.update_traffic_percentage(&deployment_id, 100.0).await?;
353            self.mark_deployment_active(&deployment_id).await?;
354        } else {
355            self.mark_deployment_failed(&deployment_id, "Health check failed").await?;
356            anyhow::bail!("Blue-green deployment failed health check");
357        }
358
359        tracing::info!(
360            "Completed blue-green deployment for {}:{}",
361            model.model_name(),
362            model.version()
363        );
364        Ok(deployment_id)
365    }
366
367    async fn deploy_rolling_update(
368        &self,
369        version_id: Uuid,
370        model: &VersionedModel,
371        config: DeploymentConfig,
372    ) -> Result<String> {
373        let deployment_id = format!("{}:rolling", model.model_name());
374
375        let deployment = ActiveDeployment {
376            deployment_id: deployment_id.clone(),
377            model_name: model.model_name().to_string(),
378            version_id,
379            environment: config.environment,
380            strategy: DeploymentStrategy::RollingUpdate,
381            status: DeploymentStatus::Deploying,
382            traffic_percentage: 0.0,
383            deployment_time: Utc::now(),
384            health_check_url: config.health_check_url,
385            rollback_version: None,
386            config_overrides: config.config_overrides,
387        };
388
389        {
390            let mut deployments = self.deployments.write().await;
391            deployments.insert(deployment_id.clone(), deployment);
392        }
393
394        // Gradual traffic increase
395        let steps = vec![25.0, 50.0, 75.0, 100.0];
396        for &percentage in &steps {
397            self.update_traffic_percentage(&deployment_id, percentage).await?;
398            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
399
400            let health = self.health_check(&deployment_id).await?;
401            if !health.is_healthy {
402                self.mark_deployment_failed(
403                    &deployment_id,
404                    "Health check failed during rolling update",
405                )
406                .await?;
407                anyhow::bail!("Rolling update failed at {}% traffic", percentage);
408            }
409        }
410
411        self.mark_deployment_active(&deployment_id).await?;
412        tracing::info!(
413            "Completed rolling update for {}:{}",
414            model.model_name(),
415            model.version()
416        );
417        Ok(deployment_id)
418    }
419
420    async fn deploy_ab_test(
421        &self,
422        version_id: Uuid,
423        model: &VersionedModel,
424        config: DeploymentConfig,
425    ) -> Result<String> {
426        let deployment_id = format!("{}:ab-test", model.model_name());
427
428        // Create A/B test experiment
429        let control_variant = Variant::new("control", "current-production");
430        let treatment_variant = Variant::new("treatment", &format!("version-{}", version_id));
431
432        let experiment_config = ExperimentConfig {
433            name: format!("A/B Test: {}", model.qualified_name()),
434            description: format!(
435                "Testing new version {} against current production",
436                model.version()
437            ),
438            control_variant,
439            treatment_variants: vec![treatment_variant],
440            traffic_percentage: config.initial_traffic_percentage.unwrap_or(50.0),
441            min_sample_size: config.min_sample_size.unwrap_or(1000),
442            max_duration_hours: config.max_duration_hours.unwrap_or(24),
443        };
444
445        let experiment_id = self.ab_test_manager.create_experiment(experiment_config)?;
446
447        let deployment = ActiveDeployment {
448            deployment_id: deployment_id.clone(),
449            model_name: model.model_name().to_string(),
450            version_id,
451            environment: config.environment,
452            strategy: DeploymentStrategy::ABTest,
453            status: DeploymentStatus::Active,
454            traffic_percentage: config.initial_traffic_percentage.unwrap_or(50.0),
455            deployment_time: Utc::now(),
456            health_check_url: config.health_check_url,
457            rollback_version: None,
458            config_overrides: config.config_overrides,
459        };
460
461        {
462            let mut deployments = self.deployments.write().await;
463            deployments.insert(deployment_id.clone(), deployment);
464        }
465
466        tracing::info!(
467            "Started A/B test deployment for {}:{} (experiment: {})",
468            model.model_name(),
469            model.version(),
470            experiment_id
471        );
472        Ok(deployment_id)
473    }
474
475    async fn mark_deployment_active(&self, deployment_id: &str) -> Result<()> {
476        let mut deployments = self.deployments.write().await;
477        if let Some(deployment) = deployments.get_mut(deployment_id) {
478            deployment.status = DeploymentStatus::Active;
479
480            self.record_deployment_event(
481                deployment_id,
482                DeploymentEvent {
483                    event_type: DeploymentEventType::Activate,
484                    version_id: deployment.version_id,
485                    timestamp: Utc::now(),
486                    message: "Deployment activated".to_string(),
487                    triggered_by: "system".to_string(),
488                    metadata: HashMap::new(),
489                },
490            )
491            .await;
492        }
493        Ok(())
494    }
495
496    async fn mark_deployment_failed(&self, deployment_id: &str, reason: &str) -> Result<()> {
497        let mut deployments = self.deployments.write().await;
498        if let Some(deployment) = deployments.get_mut(deployment_id) {
499            deployment.status = DeploymentStatus::Failed;
500
501            self.record_deployment_event(
502                deployment_id,
503                DeploymentEvent {
504                    event_type: DeploymentEventType::Fail,
505                    version_id: deployment.version_id,
506                    timestamp: Utc::now(),
507                    message: format!("Deployment failed: {}", reason),
508                    triggered_by: "system".to_string(),
509                    metadata: [("failure_reason".to_string(), reason.into())].into(),
510                },
511            )
512            .await;
513        }
514        Ok(())
515    }
516
517    async fn record_deployment_event(&self, deployment_id: &str, event: DeploymentEvent) {
518        let mut history = self.deployment_history.write().await;
519        history.entry(deployment_id.to_string()).or_default().push(event);
520    }
521}
522
523/// Active deployment information
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct ActiveDeployment {
526    pub deployment_id: String,
527    pub model_name: String,
528    pub version_id: Uuid,
529    pub environment: Environment,
530    pub strategy: DeploymentStrategy,
531    pub status: DeploymentStatus,
532    pub traffic_percentage: f64,
533    pub deployment_time: DateTime<Utc>,
534    pub health_check_url: Option<String>,
535    pub rollback_version: Option<Uuid>,
536    pub config_overrides: HashMap<String, serde_json::Value>,
537}
538
539/// Deployment configuration
540#[derive(Debug, Clone)]
541pub struct DeploymentConfig {
542    pub environment: Environment,
543    pub strategy: DeploymentStrategy,
544    pub initial_traffic_percentage: Option<f64>,
545    pub health_check_url: Option<String>,
546    pub config_overrides: HashMap<String, serde_json::Value>,
547    pub min_sample_size: Option<usize>,
548    pub max_duration_hours: Option<u64>,
549}
550
551/// Deployment environments
552#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
553pub enum Environment {
554    Development,
555    Testing,
556    Staging,
557    Production,
558    Canary,
559}
560
561impl std::fmt::Display for Environment {
562    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563        match self {
564            Environment::Development => write!(f, "development"),
565            Environment::Testing => write!(f, "testing"),
566            Environment::Staging => write!(f, "staging"),
567            Environment::Production => write!(f, "production"),
568            Environment::Canary => write!(f, "canary"),
569        }
570    }
571}
572
573/// Deployment strategies
574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
575pub enum DeploymentStrategy {
576    /// Replace all instances at once
577    BlueGreen,
578    /// Gradual rollout with increasing traffic
579    Canary,
580    /// Progressive replacement of instances
581    RollingUpdate,
582    /// A/B testing between versions
583    ABTest,
584}
585
586/// Deployment status
587#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
588pub enum DeploymentStatus {
589    Deploying,
590    Active,
591    Failed,
592    RollingBack,
593    Archived,
594}
595
596/// Deployment event types
597#[derive(Debug, Clone, Serialize, Deserialize)]
598pub enum DeploymentEventType {
599    Deploy,
600    Activate,
601    Fail,
602    Rollback,
603    TrafficUpdate,
604    HealthCheck,
605}
606
607/// Deployment event
608#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct DeploymentEvent {
610    pub event_type: DeploymentEventType,
611    pub version_id: Uuid,
612    pub timestamp: DateTime<Utc>,
613    pub message: String,
614    pub triggered_by: String,
615    pub metadata: HashMap<String, serde_json::Value>,
616}
617
618/// Health check status
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct HealthStatus {
621    pub deployment_id: String,
622    pub is_healthy: bool,
623    pub last_check: DateTime<Utc>,
624    pub response_time_ms: u64,
625    pub error_rate_percent: f64,
626    pub metrics: HashMap<String, f64>,
627}
628
629/// Deployment statistics
630#[derive(Debug, Clone, Serialize, Deserialize)]
631pub struct DeploymentStatistics {
632    pub total_deployments: usize,
633    pub active_deployments: usize,
634    pub failed_deployments: usize,
635    pub deploying_count: usize,
636    pub rolling_back_count: usize,
637    pub environments: HashMap<String, usize>,
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use crate::versioning::metadata::ModelMetadata;
644    use crate::versioning::storage::InMemoryStorage;
645
646    #[tokio::test]
647    async fn test_basic_deployment() {
648        let storage = Arc::new(InMemoryStorage::new());
649        let manager = DeploymentManager::new(storage);
650
651        let metadata = ModelMetadata::builder()
652            .description("Test model".to_string())
653            .created_by("test".to_string())
654            .model_type("transformer".to_string())
655            .build();
656
657        let model = VersionedModel::new(
658            "test_model".to_string(),
659            "1.0.0".to_string(),
660            metadata,
661            vec![],
662        );
663
664        let deployment_id = manager
665            .deploy_to_production(model.id(), &model)
666            .await
667            .expect("async operation failed");
668        assert!(!deployment_id.is_empty());
669
670        let deployment = manager
671            .get_active_deployment("test_model")
672            .await
673            .expect("async operation failed");
674        assert!(deployment.is_some());
675        assert_eq!(
676            deployment.expect("operation failed in test").status,
677            DeploymentStatus::Active
678        );
679    }
680
681    #[tokio::test]
682    async fn test_canary_deployment() {
683        let storage = Arc::new(InMemoryStorage::new());
684        let manager = DeploymentManager::new(storage);
685
686        let metadata = ModelMetadata::builder()
687            .description("Test model".to_string())
688            .created_by("test".to_string())
689            .model_type("transformer".to_string())
690            .build();
691
692        let model = VersionedModel::new(
693            "test_model".to_string(),
694            "1.0.0".to_string(),
695            metadata,
696            vec![],
697        );
698
699        let config = DeploymentConfig {
700            environment: Environment::Production,
701            strategy: DeploymentStrategy::Canary,
702            initial_traffic_percentage: Some(10.0),
703            health_check_url: None,
704            config_overrides: HashMap::new(),
705            min_sample_size: None,
706            max_duration_hours: None,
707        };
708
709        let deployment_id = manager
710            .deploy_with_strategy(model.id(), &model, config)
711            .await
712            .expect("async operation failed");
713        assert!(!deployment_id.is_empty());
714
715        // Check initial traffic percentage
716        let deployments = manager.list_deployments().await.expect("async operation failed");
717        let canary_deployment = deployments.iter().find(|d| d.deployment_id == deployment_id);
718        assert!(canary_deployment.is_some());
719        assert_eq!(
720            canary_deployment.expect("operation failed in test").traffic_percentage,
721            10.0
722        );
723    }
724
725    #[tokio::test]
726    async fn test_rollback() {
727        let storage = Arc::new(InMemoryStorage::new());
728        let manager = DeploymentManager::new(storage);
729
730        let metadata = ModelMetadata::builder()
731            .description("Test model".to_string())
732            .created_by("test".to_string())
733            .model_type("transformer".to_string())
734            .build();
735
736        let model = VersionedModel::new(
737            "test_model".to_string(),
738            "1.0.0".to_string(),
739            metadata,
740            vec![],
741        );
742
743        // Deploy initial version
744        manager
745            .deploy_to_production(model.id(), &model)
746            .await
747            .expect("async operation failed");
748
749        // Create new version
750        let new_metadata = ModelMetadata::builder()
751            .description("Updated test model".to_string())
752            .created_by("test".to_string())
753            .model_type("transformer".to_string())
754            .build();
755
756        let new_model = VersionedModel::new(
757            "test_model".to_string(),
758            "1.1.0".to_string(),
759            new_metadata,
760            vec![],
761        );
762
763        // Deploy new version
764        manager
765            .deploy_to_production(new_model.id(), &new_model)
766            .await
767            .expect("async operation failed");
768
769        // Rollback to original version
770        manager
771            .rollback("test_model", model.id())
772            .await
773            .expect("async operation failed");
774
775        let deployment = manager
776            .get_active_deployment("test_model")
777            .await
778            .expect("async operation failed");
779        assert!(deployment.is_some());
780        assert_eq!(
781            deployment.expect("operation failed in test").version_id,
782            model.id()
783        );
784    }
785
786    #[tokio::test]
787    async fn test_traffic_update() {
788        let storage = Arc::new(InMemoryStorage::new());
789        let manager = DeploymentManager::new(storage);
790
791        let metadata = ModelMetadata::builder()
792            .description("Test model".to_string())
793            .created_by("test".to_string())
794            .model_type("transformer".to_string())
795            .build();
796
797        let model = VersionedModel::new(
798            "test_model".to_string(),
799            "1.0.0".to_string(),
800            metadata,
801            vec![],
802        );
803
804        let deployment_id = manager
805            .deploy_to_production(model.id(), &model)
806            .await
807            .expect("async operation failed");
808
809        // Update traffic percentage
810        manager
811            .update_traffic_percentage(&deployment_id, 75.0)
812            .await
813            .expect("async operation failed");
814
815        let deployment = manager
816            .get_active_deployment("test_model")
817            .await
818            .expect("async operation failed");
819        assert!(deployment.is_some());
820        assert_eq!(
821            deployment.expect("operation failed in test").traffic_percentage,
822            75.0
823        );
824    }
825
826    #[tokio::test]
827    async fn test_health_check() {
828        let storage = Arc::new(InMemoryStorage::new());
829        let manager = DeploymentManager::new(storage);
830
831        let metadata = ModelMetadata::builder()
832            .description("Test model".to_string())
833            .created_by("test".to_string())
834            .model_type("transformer".to_string())
835            .build();
836
837        let model = VersionedModel::new(
838            "test_model".to_string(),
839            "1.0.0".to_string(),
840            metadata,
841            vec![],
842        );
843
844        let deployment_id = manager
845            .deploy_to_production(model.id(), &model)
846            .await
847            .expect("async operation failed");
848
849        let health = manager.health_check(&deployment_id).await.expect("async operation failed");
850        assert!(health.is_healthy);
851        assert_eq!(health.deployment_id, deployment_id);
852    }
853}