1use 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
15pub struct DeploymentManager {
17 #[allow(dead_code)] 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 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 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 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 {
77 let mut deployments = self.deployments.write().await;
78 deployments.insert(deployment_id.clone(), deployment);
79 }
80
81 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 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 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 pub async fn rollback(&self, model_name: &str, target_version_id: Uuid) -> Result<()> {
116 let deployment_id = format!("{}:production", model_name);
117
118 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 {
132 let mut deployments = self.deployments.write().await;
133 deployments.insert(deployment_id.clone(), deployment);
134 }
135
136 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 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 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 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 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 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 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 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 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 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 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 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, 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 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 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 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 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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
575pub enum DeploymentStrategy {
576 BlueGreen,
578 Canary,
580 RollingUpdate,
582 ABTest,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
588pub enum DeploymentStatus {
589 Deploying,
590 Active,
591 Failed,
592 RollingBack,
593 Archived,
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
598pub enum DeploymentEventType {
599 Deploy,
600 Activate,
601 Fail,
602 Rollback,
603 TrafficUpdate,
604 HealthCheck,
605}
606
607#[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#[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#[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 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 manager
745 .deploy_to_production(model.id(), &model)
746 .await
747 .expect("async operation failed");
748
749 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 manager
765 .deploy_to_production(new_model.id(), &new_model)
766 .await
767 .expect("async operation failed");
768
769 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 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}