Skip to main content

llm_optimizer_processor/actuator/
rollback.rs

1//! Rollback Engine for the Actuator
2//!
3//! This module provides automatic and manual rollback capabilities with:
4//! - Automatic rollback triggers (health, error rate, latency, quality, cost)
5//! - Fast rollback mode (immediate revert)
6//! - Gradual rollback (reverse canary phases)
7//! - Configuration snapshot restoration
8//! - State recovery and validation
9//! - Rollback history tracking
10//! - Notification support
11
12use 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/// Rollback mode
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum RollbackMode {
32    /// Fast rollback - immediate revert to previous configuration
33    Fast,
34    /// Gradual rollback - reverse through canary phases
35    Gradual,
36}
37
38/// Rollback phase for gradual rollback
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RollbackPhase {
41    /// Phase number (decreasing)
42    pub phase: usize,
43    /// Traffic percentage to old config
44    pub traffic_percent: f64,
45    /// Start time
46    pub start_time: Option<DateTime<Utc>>,
47    /// End time
48    pub end_time: Option<DateTime<Utc>>,
49    /// Whether this phase is complete
50    pub completed: bool,
51}
52
53/// Rollback execution status
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct RollbackStatus {
56    /// Rollback ID
57    pub rollback_id: String,
58    /// Deployment being rolled back
59    pub deployment_id: String,
60    /// Rollback mode used
61    pub mode: RollbackMode,
62    /// Current phase (for gradual rollback)
63    pub current_phase: Option<usize>,
64    /// Start time
65    pub start_time: DateTime<Utc>,
66    /// Completion time
67    pub end_time: Option<DateTime<Utc>>,
68    /// Whether rollback is complete
69    pub completed: bool,
70    /// Whether rollback was successful
71    pub success: bool,
72    /// Error message if failed
73    pub error: Option<String>,
74    /// Phases (for gradual rollback)
75    pub phases: Vec<RollbackPhase>,
76}
77
78/// Rollback history entry
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct RollbackHistoryEntry {
81    /// Rollback ID
82    pub rollback_id: String,
83    /// Deployment ID
84    pub deployment_id: String,
85    /// Request details
86    pub request: RollbackRequest,
87    /// Result
88    pub result: RollbackResult,
89    /// Status
90    pub status: RollbackStatus,
91    /// Trigger details
92    pub trigger: RollbackTrigger,
93}
94
95/// Rollback trigger details
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct RollbackTrigger {
98    /// Trigger type
99    pub trigger_type: RollbackReason,
100    /// Timestamp when triggered
101    pub timestamp: DateTime<Utc>,
102    /// Automatic or manual
103    pub automatic: bool,
104    /// Metrics at trigger time
105    pub metrics: Option<DeploymentMetrics>,
106    /// Health status at trigger time
107    pub health_status: Option<HealthStatus>,
108    /// Threshold that was exceeded (if applicable)
109    pub threshold_exceeded: Option<ThresholdViolation>,
110}
111
112/// Threshold violation details
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ThresholdViolation {
115    /// Metric name
116    pub metric: String,
117    /// Actual value
118    pub actual_value: f64,
119    /// Threshold value
120    pub threshold_value: f64,
121    /// Comparison operator
122    pub operator: String,
123}
124
125/// Rollback notification
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct RollbackNotification {
128    /// Notification ID
129    pub id: String,
130    /// Deployment ID
131    pub deployment_id: String,
132    /// Rollback ID
133    pub rollback_id: String,
134    /// Reason for rollback
135    pub reason: RollbackReason,
136    /// Timestamp
137    pub timestamp: DateTime<Utc>,
138    /// Severity
139    pub severity: NotificationSeverity,
140    /// Message
141    pub message: String,
142    /// Additional details
143    pub details: HashMap<String, serde_json::Value>,
144}
145
146/// Notification severity
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148pub enum NotificationSeverity {
149    Info,
150    Warning,
151    Error,
152    Critical,
153}
154
155/// Rollback engine statistics
156#[derive(Debug, Clone, Default, Serialize, Deserialize)]
157pub struct RollbackStats {
158    /// Total rollbacks executed
159    pub total_rollbacks: u64,
160    /// Automatic rollbacks
161    pub automatic_rollbacks: u64,
162    /// Manual rollbacks
163    pub manual_rollbacks: u64,
164    /// Successful rollbacks
165    pub successful_rollbacks: u64,
166    /// Failed rollbacks
167    pub failed_rollbacks: u64,
168    /// Fast rollbacks
169    pub fast_rollbacks: u64,
170    /// Gradual rollbacks
171    pub gradual_rollbacks: u64,
172    /// Average rollback duration (seconds)
173    pub avg_rollback_duration_secs: f64,
174    /// Fastest rollback (seconds)
175    pub fastest_rollback_secs: f64,
176    /// Rollbacks by reason
177    pub rollbacks_by_reason: HashMap<String, u64>,
178}
179
180/// Rollback Engine implementation
181pub struct RollbackEngine {
182    /// Configuration
183    config: RollbackConfig,
184    /// Configuration manager for applying/reverting changes
185    config_manager: Arc<RwLock<dyn ConfigurationManager>>,
186    /// Health monitor for validation
187    health_monitor: Arc<RwLock<dyn HealthMonitor>>,
188    /// Rollback history
189    history: Arc<RwLock<VecDeque<RollbackHistoryEntry>>>,
190    /// Active rollback status
191    active_rollbacks: Arc<RwLock<HashMap<String, RollbackStatus>>>,
192    /// Configuration snapshots
193    snapshots: Arc<RwLock<HashMap<String, ConfigurationSnapshot>>>,
194    /// Statistics
195    stats: Arc<RwLock<RollbackStats>>,
196    /// Notification handlers
197    notification_handlers: Arc<RwLock<Vec<Box<dyn NotificationHandler>>>>,
198}
199
200impl RollbackEngine {
201    /// Create a new rollback engine
202    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    /// Check if rollback should be triggered automatically
220    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        // Check health status
231        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        // Check error rate
243        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        // Check latency (P99)
260        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        // Check quality degradation
277        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        // Check cost increase
296        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    /// Execute rollback
324    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        // Determine rollback mode
334        let mode = if self.config.fast_rollback || request.reason == RollbackReason::HealthCheckFailed
335        {
336            RollbackMode::Fast
337        } else {
338            RollbackMode::Gradual
339        };
340
341        // Create rollback status
342        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        // Store active rollback
356        {
357            let mut active = self.active_rollbacks.write().await;
358            active.insert(request.deployment_id.clone(), status.clone());
359        }
360
361        // Send notification
362        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        // Execute rollback based on mode
385        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        // Update status
397        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                // Validate system health after rollback
407                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        // Update statistics
440        self.update_stats(&rollback_result, mode, request.automatic).await;
441
442        // Store in history
443        self.store_history(
444            rollback_id.clone(),
445            request.clone(),
446            rollback_result.clone(),
447            status.clone(),
448        )
449        .await?;
450
451        // Remove from active rollbacks
452        {
453            let mut active = self.active_rollbacks.write().await;
454            active.remove(&request.deployment_id);
455        }
456
457        // Send completion notification
458        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    /// Execute fast rollback (immediate revert)
484    async fn execute_fast_rollback(
485        &self,
486        request: &RollbackRequest,
487        config_changes: &[ConfigChange],
488        status: &mut RollbackStatus,
489    ) -> ActuatorResult<()> {
490        // Revert configuration changes immediately
491        let mut config_manager = self.config_manager.write().await;
492        config_manager.revert_config(config_changes).await?;
493
494        Ok(())
495    }
496
497    /// Execute gradual rollback (reverse through canary phases)
498    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        // Determine rollback phases (reverse of deployment phases)
506        let phases = if let Some(current_phase) = deployment_status.current_phase {
507            // Reverse from current phase back to 0%
508            (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        // Execute each phase
538        for (idx, phase) in status.phases.iter_mut().enumerate() {
539            status.current_phase = Some(idx);
540            phase.start_time = Some(Utc::now());
541
542            // Gradually shift traffic back
543            // In a real implementation, this would interact with traffic routing
544            tokio::time::sleep(Duration::from_secs(10)).await;
545
546            phase.end_time = Some(Utc::now());
547            phase.completed = true;
548        }
549
550        // Finally, revert the configuration changes
551        let mut config_manager = self.config_manager.write().await;
552        config_manager.revert_config(config_changes).await?;
553
554        Ok(())
555    }
556
557    /// Validate system health after rollback
558    async fn validate_after_rollback(&self, deployment_id: &str) -> ActuatorResult<bool> {
559        // Wait a brief period for system to stabilize
560        tokio::time::sleep(Duration::from_secs(5)).await;
561
562        // Check health
563        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    /// Store snapshot before deployment
570    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    /// Restore from snapshot
585    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    /// Get rollback history
606    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    /// Get active rollback status
613    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    /// Get statistics
622    pub async fn get_stats(&self) -> RollbackStats {
623        self.stats.read().await.clone()
624    }
625
626    /// Add notification handler
627    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    /// Send notification
636    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    /// Store rollback in history
645    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        // Trim history to max size
674        while history.len() > self.config.max_rollback_history {
675            history.pop_back();
676        }
677
678        Ok(())
679    }
680
681    /// Update statistics
682    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        // Update average duration
707        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/// Trait for notification handlers
724#[async_trait]
725pub trait NotificationHandler: Send + Sync {
726    /// Handle a rollback notification
727    async fn handle(&self, notification: RollbackNotification) -> ActuatorResult<()>;
728}
729
730/// Mock configuration manager for testing
731#[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/// Mock health monitor for testing
770#[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        // Execute a few rollbacks
1065        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        // Should be removed from active after completion
1126        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}