1use async_trait::async_trait;
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, VecDeque};
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::sync::RwLock;
19
20use super::config::RollbackConfig;
21use super::error::{ActuatorError, ActuatorResult, DeploymentState};
22use super::traits::{ConfigurationManager, HealthMonitor};
23use super::types::{
24 ConfigurationSnapshot, DeploymentMetrics, DeploymentStatus, HealthStatus, RollbackReason,
25 RollbackRequest, RollbackResult,
26};
27use crate::decision::{ConfigChange, SystemMetrics};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum RollbackMode {
32 Fast,
34 Gradual,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RollbackPhase {
41 pub phase: usize,
43 pub traffic_percent: f64,
45 pub start_time: Option<DateTime<Utc>>,
47 pub end_time: Option<DateTime<Utc>>,
49 pub completed: bool,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct RollbackStatus {
56 pub rollback_id: String,
58 pub deployment_id: String,
60 pub mode: RollbackMode,
62 pub current_phase: Option<usize>,
64 pub start_time: DateTime<Utc>,
66 pub end_time: Option<DateTime<Utc>>,
68 pub completed: bool,
70 pub success: bool,
72 pub error: Option<String>,
74 pub phases: Vec<RollbackPhase>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct RollbackHistoryEntry {
81 pub rollback_id: String,
83 pub deployment_id: String,
85 pub request: RollbackRequest,
87 pub result: RollbackResult,
89 pub status: RollbackStatus,
91 pub trigger: RollbackTrigger,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct RollbackTrigger {
98 pub trigger_type: RollbackReason,
100 pub timestamp: DateTime<Utc>,
102 pub automatic: bool,
104 pub metrics: Option<DeploymentMetrics>,
106 pub health_status: Option<HealthStatus>,
108 pub threshold_exceeded: Option<ThresholdViolation>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ThresholdViolation {
115 pub metric: String,
117 pub actual_value: f64,
119 pub threshold_value: f64,
121 pub operator: String,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct RollbackNotification {
128 pub id: String,
130 pub deployment_id: String,
132 pub rollback_id: String,
134 pub reason: RollbackReason,
136 pub timestamp: DateTime<Utc>,
138 pub severity: NotificationSeverity,
140 pub message: String,
142 pub details: HashMap<String, serde_json::Value>,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148pub enum NotificationSeverity {
149 Info,
150 Warning,
151 Error,
152 Critical,
153}
154
155#[derive(Debug, Clone, Default, Serialize, Deserialize)]
157pub struct RollbackStats {
158 pub total_rollbacks: u64,
160 pub automatic_rollbacks: u64,
162 pub manual_rollbacks: u64,
164 pub successful_rollbacks: u64,
166 pub failed_rollbacks: u64,
168 pub fast_rollbacks: u64,
170 pub gradual_rollbacks: u64,
172 pub avg_rollback_duration_secs: f64,
174 pub fastest_rollback_secs: f64,
176 pub rollbacks_by_reason: HashMap<String, u64>,
178}
179
180pub struct RollbackEngine {
182 config: RollbackConfig,
184 config_manager: Arc<RwLock<dyn ConfigurationManager>>,
186 health_monitor: Arc<RwLock<dyn HealthMonitor>>,
188 history: Arc<RwLock<VecDeque<RollbackHistoryEntry>>>,
190 active_rollbacks: Arc<RwLock<HashMap<String, RollbackStatus>>>,
192 snapshots: Arc<RwLock<HashMap<String, ConfigurationSnapshot>>>,
194 stats: Arc<RwLock<RollbackStats>>,
196 notification_handlers: Arc<RwLock<Vec<Box<dyn NotificationHandler>>>>,
198}
199
200impl RollbackEngine {
201 pub fn new(
203 config: RollbackConfig,
204 config_manager: Arc<RwLock<dyn ConfigurationManager>>,
205 health_monitor: Arc<RwLock<dyn HealthMonitor>>,
206 ) -> Self {
207 Self {
208 config,
209 config_manager,
210 health_monitor,
211 history: Arc::new(RwLock::new(VecDeque::new())),
212 active_rollbacks: Arc::new(RwLock::new(HashMap::new())),
213 snapshots: Arc::new(RwLock::new(HashMap::new())),
214 stats: Arc::new(RwLock::new(RollbackStats::default())),
215 notification_handlers: Arc::new(RwLock::new(Vec::new())),
216 }
217 }
218
219 pub async fn should_trigger_rollback(
221 &self,
222 deployment_id: &str,
223 status: &DeploymentStatus,
224 metrics: &SystemMetrics,
225 ) -> ActuatorResult<Option<RollbackTrigger>> {
226 if !self.config.auto_rollback {
227 return Ok(None);
228 }
229
230 if !status.health.healthy {
232 return Ok(Some(RollbackTrigger {
233 trigger_type: RollbackReason::HealthCheckFailed,
234 timestamp: Utc::now(),
235 automatic: true,
236 metrics: Some(status.metrics.clone()),
237 health_status: Some(status.health.clone()),
238 threshold_exceeded: None,
239 }));
240 }
241
242 if metrics.error_rate_pct > 5.0 {
244 return Ok(Some(RollbackTrigger {
245 trigger_type: RollbackReason::HighErrorRate,
246 timestamp: Utc::now(),
247 automatic: true,
248 metrics: Some(status.metrics.clone()),
249 health_status: Some(status.health.clone()),
250 threshold_exceeded: Some(ThresholdViolation {
251 metric: "error_rate_pct".to_string(),
252 actual_value: metrics.error_rate_pct,
253 threshold_value: 5.0,
254 operator: ">".to_string(),
255 }),
256 }));
257 }
258
259 if metrics.p99_latency_ms > 2000.0 {
261 return Ok(Some(RollbackTrigger {
262 trigger_type: RollbackReason::HighLatency,
263 timestamp: Utc::now(),
264 automatic: true,
265 metrics: Some(status.metrics.clone()),
266 health_status: Some(status.health.clone()),
267 threshold_exceeded: Some(ThresholdViolation {
268 metric: "p99_latency_ms".to_string(),
269 actual_value: metrics.p99_latency_ms,
270 threshold_value: 2000.0,
271 operator: ">".to_string(),
272 }),
273 }));
274 }
275
276 if let Some(avg_rating) = metrics.avg_rating {
278 if avg_rating < 3.5 {
279 return Ok(Some(RollbackTrigger {
280 trigger_type: RollbackReason::QualityDegradation,
281 timestamp: Utc::now(),
282 automatic: true,
283 metrics: Some(status.metrics.clone()),
284 health_status: Some(status.health.clone()),
285 threshold_exceeded: Some(ThresholdViolation {
286 metric: "avg_rating".to_string(),
287 actual_value: avg_rating,
288 threshold_value: 3.5,
289 operator: "<".to_string(),
290 }),
291 }));
292 }
293 }
294
295 let cost_increase_pct = if status.metrics.control_cost_per_request > 0.0 {
297 ((status.metrics.canary_cost_per_request - status.metrics.control_cost_per_request)
298 / status.metrics.control_cost_per_request)
299 * 100.0
300 } else {
301 0.0
302 };
303
304 if cost_increase_pct > 20.0 {
305 return Ok(Some(RollbackTrigger {
306 trigger_type: RollbackReason::HighCost,
307 timestamp: Utc::now(),
308 automatic: true,
309 metrics: Some(status.metrics.clone()),
310 health_status: Some(status.health.clone()),
311 threshold_exceeded: Some(ThresholdViolation {
312 metric: "cost_increase_pct".to_string(),
313 actual_value: cost_increase_pct,
314 threshold_value: 20.0,
315 operator: ">".to_string(),
316 }),
317 }));
318 }
319
320 Ok(None)
321 }
322
323 pub async fn execute_rollback(
325 &self,
326 request: RollbackRequest,
327 deployment_status: &DeploymentStatus,
328 config_changes: Vec<ConfigChange>,
329 ) -> ActuatorResult<RollbackResult> {
330 let start_time = Utc::now();
331 let rollback_id = format!("rollback-{}", uuid::Uuid::new_v4());
332
333 let mode = if self.config.fast_rollback || request.reason == RollbackReason::HealthCheckFailed
335 {
336 RollbackMode::Fast
337 } else {
338 RollbackMode::Gradual
339 };
340
341 let mut status = RollbackStatus {
343 rollback_id: rollback_id.clone(),
344 deployment_id: request.deployment_id.clone(),
345 mode,
346 current_phase: None,
347 start_time,
348 end_time: None,
349 completed: false,
350 success: false,
351 error: None,
352 phases: Vec::new(),
353 };
354
355 {
357 let mut active = self.active_rollbacks.write().await;
358 active.insert(request.deployment_id.clone(), status.clone());
359 }
360
361 if self.config.enable_notifications {
363 let notification = RollbackNotification {
364 id: format!("notif-{}", uuid::Uuid::new_v4()),
365 deployment_id: request.deployment_id.clone(),
366 rollback_id: rollback_id.clone(),
367 reason: request.reason.clone(),
368 timestamp: Utc::now(),
369 severity: if request.automatic {
370 NotificationSeverity::Warning
371 } else {
372 NotificationSeverity::Info
373 },
374 message: format!(
375 "Rollback initiated for deployment {} ({})",
376 request.deployment_id,
377 if request.automatic { "automatic" } else { "manual" }
378 ),
379 details: request.metadata.clone(),
380 };
381 self.send_notification(notification).await?;
382 }
383
384 let result = match mode {
386 RollbackMode::Fast => {
387 self.execute_fast_rollback(&request, &config_changes, &mut status)
388 .await
389 }
390 RollbackMode::Gradual => {
391 self.execute_gradual_rollback(&request, &config_changes, deployment_status, &mut status)
392 .await
393 }
394 };
395
396 let end_time = Utc::now();
398 let duration = end_time.signed_duration_since(start_time);
399 status.end_time = Some(end_time);
400 status.completed = true;
401
402 let rollback_result = match result {
403 Ok(_) => {
404 status.success = true;
405
406 let validation_result = self.validate_after_rollback(&request.deployment_id).await;
408
409 let success = validation_result.unwrap_or(false);
410 if !success {
411 status.success = false;
412 status.error = Some("Post-rollback validation failed".to_string());
413 }
414
415 RollbackResult {
416 deployment_id: request.deployment_id.clone(),
417 success,
418 error: status.error.clone(),
419 reverted_changes: config_changes.clone(),
420 timestamp: end_time,
421 duration: Duration::from_secs(duration.num_seconds().max(0) as u64),
422 }
423 }
424 Err(e) => {
425 status.success = false;
426 status.error = Some(e.to_string());
427
428 RollbackResult {
429 deployment_id: request.deployment_id.clone(),
430 success: false,
431 error: Some(e.to_string()),
432 reverted_changes: Vec::new(),
433 timestamp: end_time,
434 duration: Duration::from_secs(duration.num_seconds().max(0) as u64),
435 }
436 }
437 };
438
439 self.update_stats(&rollback_result, mode, request.automatic).await;
441
442 self.store_history(
444 rollback_id.clone(),
445 request.clone(),
446 rollback_result.clone(),
447 status.clone(),
448 )
449 .await?;
450
451 {
453 let mut active = self.active_rollbacks.write().await;
454 active.remove(&request.deployment_id);
455 }
456
457 if self.config.enable_notifications {
459 let notification = RollbackNotification {
460 id: format!("notif-{}", uuid::Uuid::new_v4()),
461 deployment_id: request.deployment_id.clone(),
462 rollback_id,
463 reason: request.reason,
464 timestamp: Utc::now(),
465 severity: if rollback_result.success {
466 NotificationSeverity::Info
467 } else {
468 NotificationSeverity::Critical
469 },
470 message: format!(
471 "Rollback {} for deployment {}",
472 if rollback_result.success { "completed" } else { "failed" },
473 request.deployment_id
474 ),
475 details: HashMap::new(),
476 };
477 self.send_notification(notification).await?;
478 }
479
480 Ok(rollback_result)
481 }
482
483 async fn execute_fast_rollback(
485 &self,
486 request: &RollbackRequest,
487 config_changes: &[ConfigChange],
488 status: &mut RollbackStatus,
489 ) -> ActuatorResult<()> {
490 let mut config_manager = self.config_manager.write().await;
492 config_manager.revert_config(config_changes).await?;
493
494 Ok(())
495 }
496
497 async fn execute_gradual_rollback(
499 &self,
500 request: &RollbackRequest,
501 config_changes: &[ConfigChange],
502 deployment_status: &DeploymentStatus,
503 status: &mut RollbackStatus,
504 ) -> ActuatorResult<()> {
505 let phases = if let Some(current_phase) = deployment_status.current_phase {
507 (0..=current_phase)
509 .rev()
510 .map(|phase| RollbackPhase {
511 phase,
512 traffic_percent: match phase {
513 0 => 0.0,
514 1 => 1.0,
515 2 => 5.0,
516 3 => 25.0,
517 4 => 50.0,
518 _ => 100.0,
519 },
520 start_time: None,
521 end_time: None,
522 completed: false,
523 })
524 .collect()
525 } else {
526 vec![RollbackPhase {
527 phase: 0,
528 traffic_percent: 0.0,
529 start_time: None,
530 end_time: None,
531 completed: false,
532 }]
533 };
534
535 status.phases = phases;
536
537 for (idx, phase) in status.phases.iter_mut().enumerate() {
539 status.current_phase = Some(idx);
540 phase.start_time = Some(Utc::now());
541
542 tokio::time::sleep(Duration::from_secs(10)).await;
545
546 phase.end_time = Some(Utc::now());
547 phase.completed = true;
548 }
549
550 let mut config_manager = self.config_manager.write().await;
552 config_manager.revert_config(config_changes).await?;
553
554 Ok(())
555 }
556
557 async fn validate_after_rollback(&self, deployment_id: &str) -> ActuatorResult<bool> {
559 tokio::time::sleep(Duration::from_secs(5)).await;
561
562 let health_monitor = self.health_monitor.read().await;
564 let health_summary = health_monitor.get_health_summary(deployment_id).await?;
565
566 Ok(health_summary.healthy)
567 }
568
569 pub async fn store_snapshot(
571 &self,
572 deployment_id: &str,
573 snapshot: ConfigurationSnapshot,
574 ) -> ActuatorResult<()> {
575 if !self.config.enable_backup {
576 return Ok(());
577 }
578
579 let mut snapshots = self.snapshots.write().await;
580 snapshots.insert(deployment_id.to_string(), snapshot);
581 Ok(())
582 }
583
584 pub async fn restore_from_snapshot(&self, deployment_id: &str) -> ActuatorResult<()> {
586 let snapshot = {
587 let snapshots = self.snapshots.read().await;
588 snapshots
589 .get(deployment_id)
590 .cloned()
591 .ok_or_else(|| {
592 ActuatorError::RollbackFailed(format!(
593 "No snapshot found for deployment {}",
594 deployment_id
595 ))
596 })?
597 };
598
599 let mut config_manager = self.config_manager.write().await;
600 config_manager.restore_snapshot(&snapshot.id).await?;
601
602 Ok(())
603 }
604
605 pub async fn get_history(&self, limit: Option<usize>) -> Vec<RollbackHistoryEntry> {
607 let history = self.history.read().await;
608 let limit = limit.unwrap_or(100);
609 history.iter().take(limit).cloned().collect()
610 }
611
612 pub async fn get_active_rollback(
614 &self,
615 deployment_id: &str,
616 ) -> Option<RollbackStatus> {
617 let active = self.active_rollbacks.read().await;
618 active.get(deployment_id).cloned()
619 }
620
621 pub async fn get_stats(&self) -> RollbackStats {
623 self.stats.read().await.clone()
624 }
625
626 pub async fn add_notification_handler(
628 &self,
629 handler: Box<dyn NotificationHandler>,
630 ) {
631 let mut handlers = self.notification_handlers.write().await;
632 handlers.push(handler);
633 }
634
635 async fn send_notification(&self, notification: RollbackNotification) -> ActuatorResult<()> {
637 let handlers = self.notification_handlers.read().await;
638 for handler in handlers.iter() {
639 handler.handle(notification.clone()).await?;
640 }
641 Ok(())
642 }
643
644 async fn store_history(
646 &self,
647 rollback_id: String,
648 request: RollbackRequest,
649 result: RollbackResult,
650 status: RollbackStatus,
651 ) -> ActuatorResult<()> {
652 let trigger = RollbackTrigger {
653 trigger_type: request.reason.clone(),
654 timestamp: request.timestamp,
655 automatic: request.automatic,
656 metrics: None,
657 health_status: None,
658 threshold_exceeded: None,
659 };
660
661 let entry = RollbackHistoryEntry {
662 rollback_id,
663 deployment_id: request.deployment_id.clone(),
664 request,
665 result,
666 status,
667 trigger,
668 };
669
670 let mut history = self.history.write().await;
671 history.push_front(entry);
672
673 while history.len() > self.config.max_rollback_history {
675 history.pop_back();
676 }
677
678 Ok(())
679 }
680
681 async fn update_stats(&self, result: &RollbackResult, mode: RollbackMode, automatic: bool) {
683 let mut stats = self.stats.write().await;
684
685 stats.total_rollbacks += 1;
686
687 if automatic {
688 stats.automatic_rollbacks += 1;
689 } else {
690 stats.manual_rollbacks += 1;
691 }
692
693 if result.success {
694 stats.successful_rollbacks += 1;
695 } else {
696 stats.failed_rollbacks += 1;
697 }
698
699 match mode {
700 RollbackMode::Fast => stats.fast_rollbacks += 1,
701 RollbackMode::Gradual => stats.gradual_rollbacks += 1,
702 }
703
704 let duration_secs = result.duration.as_secs_f64();
705
706 if stats.total_rollbacks == 1 {
708 stats.avg_rollback_duration_secs = duration_secs;
709 stats.fastest_rollback_secs = duration_secs;
710 } else {
711 stats.avg_rollback_duration_secs = (stats.avg_rollback_duration_secs
712 * (stats.total_rollbacks - 1) as f64
713 + duration_secs)
714 / stats.total_rollbacks as f64;
715
716 if duration_secs < stats.fastest_rollback_secs {
717 stats.fastest_rollback_secs = duration_secs;
718 }
719 }
720 }
721}
722
723#[async_trait]
725pub trait NotificationHandler: Send + Sync {
726 async fn handle(&self, notification: RollbackNotification) -> ActuatorResult<()>;
728}
729
730#[cfg(test)]
732pub struct MockConfigManager;
733
734#[cfg(test)]
735#[async_trait]
736impl ConfigurationManager for MockConfigManager {
737 async fn apply_config(&mut self, _changes: &[ConfigChange]) -> ActuatorResult<()> {
738 Ok(())
739 }
740
741 async fn revert_config(&mut self, _changes: &[ConfigChange]) -> ActuatorResult<()> {
742 Ok(())
743 }
744
745 async fn create_snapshot(&mut self) -> ActuatorResult<ConfigurationSnapshot> {
746 Ok(ConfigurationSnapshot {
747 id: "test-snapshot".to_string(),
748 timestamp: Utc::now(),
749 config: HashMap::new(),
750 deployment_id: None,
751 version: 1,
752 tags: Vec::new(),
753 })
754 }
755
756 async fn restore_snapshot(&mut self, _snapshot_id: &str) -> ActuatorResult<()> {
757 Ok(())
758 }
759
760 async fn get_config(&self) -> ActuatorResult<HashMap<String, serde_json::Value>> {
761 Ok(HashMap::new())
762 }
763
764 async fn validate_config(&self, _changes: &[ConfigChange]) -> ActuatorResult<()> {
765 Ok(())
766 }
767}
768
769#[cfg(test)]
771pub struct MockHealthMonitor {
772 pub healthy: bool,
773}
774
775#[cfg(test)]
776#[async_trait]
777impl HealthMonitor for MockHealthMonitor {
778 async fn check_health(
779 &self,
780 _deployment_id: &str,
781 _metrics: &SystemMetrics,
782 ) -> ActuatorResult<Vec<super::types::HealthCheckResult>> {
783 Ok(Vec::new())
784 }
785
786 async fn should_rollback(
787 &self,
788 _deployment_id: &str,
789 _status: &DeploymentStatus,
790 ) -> ActuatorResult<bool> {
791 Ok(!self.healthy)
792 }
793
794 async fn get_health_summary(
795 &self,
796 _deployment_id: &str,
797 ) -> ActuatorResult<HealthStatus> {
798 Ok(HealthStatus {
799 healthy: self.healthy,
800 checks_passed: if self.healthy { 5 } else { 0 },
801 checks_failed: if self.healthy { 0 } else { 5 },
802 check_results: Vec::new(),
803 last_check_time: Utc::now(),
804 })
805 }
806}
807
808#[cfg(test)]
809mod tests {
810 use super::*;
811
812 fn create_test_engine() -> RollbackEngine {
813 let config = RollbackConfig::default();
814 let config_manager = Arc::new(RwLock::new(MockConfigManager));
815 let health_monitor = Arc::new(RwLock::new(MockHealthMonitor { healthy: true }));
816
817 RollbackEngine::new(config, config_manager, health_monitor)
818 }
819
820 fn create_test_deployment_status() -> DeploymentStatus {
821 DeploymentStatus {
822 deployment_id: "test-deployment".to_string(),
823 state: DeploymentState::RollingOut,
824 current_phase: Some(2),
825 traffic_percent: 25.0,
826 start_time: Utc::now(),
827 phase_start_time: Some(Utc::now()),
828 health: HealthStatus {
829 healthy: true,
830 checks_passed: 5,
831 checks_failed: 0,
832 check_results: Vec::new(),
833 last_check_time: Utc::now(),
834 },
835 metrics: DeploymentMetrics::default(),
836 errors: Vec::new(),
837 last_updated: Utc::now(),
838 }
839 }
840
841 fn create_test_metrics() -> SystemMetrics {
842 SystemMetrics {
843 avg_latency_ms: 100.0,
844 p95_latency_ms: 200.0,
845 p99_latency_ms: 300.0,
846 success_rate_pct: 99.5,
847 error_rate_pct: 0.5,
848 throughput_rps: 1000.0,
849 total_cost_usd: 100.0,
850 daily_cost_usd: 10.0,
851 avg_rating: Some(4.5),
852 cache_hit_rate_pct: Some(80.0),
853 request_count: 10000,
854 timestamp: Utc::now(),
855 }
856 }
857
858 #[tokio::test]
859 async fn test_no_rollback_trigger_when_healthy() {
860 let engine = create_test_engine();
861 let status = create_test_deployment_status();
862 let metrics = create_test_metrics();
863
864 let trigger = engine
865 .should_trigger_rollback("test-deployment", &status, &metrics)
866 .await
867 .unwrap();
868
869 assert!(trigger.is_none());
870 }
871
872 #[tokio::test]
873 async fn test_rollback_trigger_on_high_error_rate() {
874 let engine = create_test_engine();
875 let status = create_test_deployment_status();
876 let mut metrics = create_test_metrics();
877 metrics.error_rate_pct = 10.0;
878
879 let trigger = engine
880 .should_trigger_rollback("test-deployment", &status, &metrics)
881 .await
882 .unwrap();
883
884 assert!(trigger.is_some());
885 let trigger = trigger.unwrap();
886 assert_eq!(trigger.trigger_type, RollbackReason::HighErrorRate);
887 assert!(trigger.automatic);
888 }
889
890 #[tokio::test]
891 async fn test_rollback_trigger_on_high_latency() {
892 let engine = create_test_engine();
893 let status = create_test_deployment_status();
894 let mut metrics = create_test_metrics();
895 metrics.p99_latency_ms = 3000.0;
896
897 let trigger = engine
898 .should_trigger_rollback("test-deployment", &status, &metrics)
899 .await
900 .unwrap();
901
902 assert!(trigger.is_some());
903 let trigger = trigger.unwrap();
904 assert_eq!(trigger.trigger_type, RollbackReason::HighLatency);
905 }
906
907 #[tokio::test]
908 async fn test_rollback_trigger_on_quality_degradation() {
909 let engine = create_test_engine();
910 let status = create_test_deployment_status();
911 let mut metrics = create_test_metrics();
912 metrics.avg_rating = Some(2.5);
913
914 let trigger = engine
915 .should_trigger_rollback("test-deployment", &status, &metrics)
916 .await
917 .unwrap();
918
919 assert!(trigger.is_some());
920 let trigger = trigger.unwrap();
921 assert_eq!(trigger.trigger_type, RollbackReason::QualityDegradation);
922 }
923
924 #[tokio::test]
925 async fn test_rollback_trigger_on_health_check_failure() {
926 let engine = create_test_engine();
927 let mut status = create_test_deployment_status();
928 status.health.healthy = false;
929 let metrics = create_test_metrics();
930
931 let trigger = engine
932 .should_trigger_rollback("test-deployment", &status, &metrics)
933 .await
934 .unwrap();
935
936 assert!(trigger.is_some());
937 let trigger = trigger.unwrap();
938 assert_eq!(trigger.trigger_type, RollbackReason::HealthCheckFailed);
939 }
940
941 #[tokio::test]
942 async fn test_fast_rollback_execution() {
943 let engine = create_test_engine();
944 let status = create_test_deployment_status();
945
946 let request = RollbackRequest {
947 deployment_id: "test-deployment".to_string(),
948 reason: RollbackReason::HealthCheckFailed,
949 timestamp: Utc::now(),
950 automatic: true,
951 metadata: HashMap::new(),
952 };
953
954 let config_changes = vec![ConfigChange {
955 config_type: crate::decision::ConfigType::Model,
956 path: "model.name".to_string(),
957 old_value: Some(serde_json::json!("gpt-3.5-turbo")),
958 new_value: serde_json::json!("gpt-4"),
959 description: "Model upgrade".to_string(),
960 }];
961
962 let result = engine
963 .execute_rollback(request, &status, config_changes.clone())
964 .await
965 .unwrap();
966
967 assert!(result.success);
968 assert_eq!(result.reverted_changes.len(), config_changes.len());
969 }
970
971 #[tokio::test]
972 async fn test_gradual_rollback_execution() {
973 let mut config = RollbackConfig::default();
974 config.fast_rollback = false;
975
976 let config_manager = Arc::new(RwLock::new(MockConfigManager));
977 let health_monitor = Arc::new(RwLock::new(MockHealthMonitor { healthy: true }));
978 let engine = RollbackEngine::new(config, config_manager, health_monitor);
979
980 let status = create_test_deployment_status();
981
982 let request = RollbackRequest {
983 deployment_id: "test-deployment".to_string(),
984 reason: RollbackReason::Manual,
985 timestamp: Utc::now(),
986 automatic: false,
987 metadata: HashMap::new(),
988 };
989
990 let config_changes = vec![ConfigChange {
991 config_type: crate::decision::ConfigType::Model,
992 path: "model.name".to_string(),
993 old_value: Some(serde_json::json!("gpt-3.5-turbo")),
994 new_value: serde_json::json!("gpt-4"),
995 description: "Model upgrade".to_string(),
996 }];
997
998 let result = engine
999 .execute_rollback(request, &status, config_changes)
1000 .await
1001 .unwrap();
1002
1003 assert!(result.success);
1004 }
1005
1006 #[tokio::test]
1007 async fn test_snapshot_storage_and_restoration() {
1008 let engine = create_test_engine();
1009
1010 let snapshot = ConfigurationSnapshot {
1011 id: "snapshot-1".to_string(),
1012 timestamp: Utc::now(),
1013 config: HashMap::new(),
1014 deployment_id: Some("test-deployment".to_string()),
1015 version: 1,
1016 tags: vec!["production".to_string()],
1017 };
1018
1019 engine
1020 .store_snapshot("test-deployment", snapshot)
1021 .await
1022 .unwrap();
1023
1024 let result = engine.restore_from_snapshot("test-deployment").await;
1025 assert!(result.is_ok());
1026 }
1027
1028 #[tokio::test]
1029 async fn test_rollback_history_tracking() {
1030 let engine = create_test_engine();
1031 let status = create_test_deployment_status();
1032
1033 let request = RollbackRequest {
1034 deployment_id: "test-deployment".to_string(),
1035 reason: RollbackReason::Manual,
1036 timestamp: Utc::now(),
1037 automatic: false,
1038 metadata: HashMap::new(),
1039 };
1040
1041 let config_changes = vec![ConfigChange {
1042 config_type: crate::decision::ConfigType::Model,
1043 path: "model.name".to_string(),
1044 old_value: Some(serde_json::json!("gpt-3.5-turbo")),
1045 new_value: serde_json::json!("gpt-4"),
1046 description: "Model upgrade".to_string(),
1047 }];
1048
1049 engine
1050 .execute_rollback(request, &status, config_changes)
1051 .await
1052 .unwrap();
1053
1054 let history = engine.get_history(None).await;
1055 assert_eq!(history.len(), 1);
1056 assert_eq!(history[0].deployment_id, "test-deployment");
1057 }
1058
1059 #[tokio::test]
1060 async fn test_rollback_statistics() {
1061 let engine = create_test_engine();
1062 let status = create_test_deployment_status();
1063
1064 for i in 0..3 {
1066 let request = RollbackRequest {
1067 deployment_id: format!("test-deployment-{}", i),
1068 reason: if i % 2 == 0 {
1069 RollbackReason::Manual
1070 } else {
1071 RollbackReason::HighErrorRate
1072 },
1073 timestamp: Utc::now(),
1074 automatic: i % 2 != 0,
1075 metadata: HashMap::new(),
1076 };
1077
1078 let config_changes = vec![ConfigChange {
1079 config_type: crate::decision::ConfigType::Model,
1080 path: "model.name".to_string(),
1081 old_value: Some(serde_json::json!("gpt-3.5-turbo")),
1082 new_value: serde_json::json!("gpt-4"),
1083 description: "Model upgrade".to_string(),
1084 }];
1085
1086 engine
1087 .execute_rollback(request, &status, config_changes)
1088 .await
1089 .unwrap();
1090 }
1091
1092 let stats = engine.get_stats().await;
1093 assert_eq!(stats.total_rollbacks, 3);
1094 assert_eq!(stats.manual_rollbacks, 2);
1095 assert_eq!(stats.automatic_rollbacks, 1);
1096 assert_eq!(stats.successful_rollbacks, 3);
1097 }
1098
1099 #[tokio::test]
1100 async fn test_active_rollback_tracking() {
1101 let engine = create_test_engine();
1102 let status = create_test_deployment_status();
1103
1104 let request = RollbackRequest {
1105 deployment_id: "test-deployment".to_string(),
1106 reason: RollbackReason::Manual,
1107 timestamp: Utc::now(),
1108 automatic: false,
1109 metadata: HashMap::new(),
1110 };
1111
1112 let config_changes = vec![ConfigChange {
1113 config_type: crate::decision::ConfigType::Model,
1114 path: "model.name".to_string(),
1115 old_value: Some(serde_json::json!("gpt-3.5-turbo")),
1116 new_value: serde_json::json!("gpt-4"),
1117 description: "Model upgrade".to_string(),
1118 }];
1119
1120 engine
1121 .execute_rollback(request, &status, config_changes)
1122 .await
1123 .unwrap();
1124
1125 let active = engine.get_active_rollback("test-deployment").await;
1127 assert!(active.is_none());
1128 }
1129
1130 #[tokio::test]
1131 async fn test_post_rollback_validation() {
1132 let config_manager = Arc::new(RwLock::new(MockConfigManager));
1133 let health_monitor = Arc::new(RwLock::new(MockHealthMonitor { healthy: true }));
1134 let engine = RollbackEngine::new(
1135 RollbackConfig::default(),
1136 config_manager,
1137 health_monitor,
1138 );
1139
1140 let result = engine.validate_after_rollback("test-deployment").await;
1141 assert!(result.is_ok());
1142 assert!(result.unwrap());
1143 }
1144
1145 #[tokio::test]
1146 async fn test_post_rollback_validation_failure() {
1147 let config_manager = Arc::new(RwLock::new(MockConfigManager));
1148 let health_monitor = Arc::new(RwLock::new(MockHealthMonitor { healthy: false }));
1149 let engine = RollbackEngine::new(
1150 RollbackConfig::default(),
1151 config_manager,
1152 health_monitor,
1153 );
1154
1155 let result = engine.validate_after_rollback("test-deployment").await;
1156 assert!(result.is_ok());
1157 assert!(!result.unwrap());
1158 }
1159}