Skip to main content

llm_optimizer_processor/actuator/
canary.rs

1//! Canary Deployment Engine
2//!
3//! This module provides a production-ready canary deployment engine that:
4//! - Manages multi-phase rollouts (1% → 5% → 25% → 50% → 100%)
5//! - Implements traffic splitting with multiple strategies
6//! - Monitors health and validates success criteria
7//! - Auto-promotes and auto-rollbacks based on metrics
8//! - Provides detailed status tracking and reporting
9
10use async_trait::async_trait;
11use chrono::{DateTime, Utc};
12use rand::Rng;
13use serde::{Deserialize, Serialize};
14use std::collections::{HashMap, VecDeque};
15use std::hash::{Hash, Hasher};
16use std::sync::Arc;
17use tokio::sync::RwLock;
18use tracing::{debug, error, info, warn};
19
20use super::error::{ActuatorError, ActuatorResult, DeploymentState};
21use super::traits::{CanaryDeployment, HealthMonitor, TrafficSplitter};
22use super::types::{
23    CanaryConfig, DeploymentMetrics, DeploymentRequest, DeploymentStatus, HealthCheckResult,
24    HealthStatus, RollbackReason, RollbackRequest, RollbackResult, StatisticalAnalysis,
25    SuccessCriteria, TrafficSplitStrategy,
26};
27
28/// Canary deployment engine state
29#[derive(Debug)]
30struct CanaryDeploymentState {
31    /// Deployment request
32    request: DeploymentRequest,
33
34    /// Canary configuration
35    config: CanaryConfig,
36
37    /// Current phase index
38    current_phase: usize,
39
40    /// Phase start time
41    phase_start_time: DateTime<Utc>,
42
43    /// Deployment start time
44    deployment_start_time: DateTime<Utc>,
45
46    /// Current deployment state
47    state: DeploymentState,
48
49    /// Accumulated metrics
50    metrics: DeploymentMetrics,
51
52    /// Health status
53    health: HealthStatus,
54
55    /// Errors encountered
56    errors: Vec<String>,
57
58    /// Manual control flags
59    paused: bool,
60
61    /// Traffic splitter for routing
62    traffic_split_counter: u64,
63
64    /// Last updated timestamp
65    last_updated: DateTime<Utc>,
66}
67
68/// Canary deployment engine
69#[derive(Clone)]
70pub struct CanaryDeploymentEngine {
71    /// Active deployments
72    deployments: Arc<RwLock<HashMap<String, CanaryDeploymentState>>>,
73
74    /// Health monitor (optional integration)
75    health_monitor: Option<Arc<dyn HealthMonitor>>,
76
77    /// Default canary configuration
78    default_config: CanaryConfig,
79
80    /// Metrics buffer for statistical analysis
81    metrics_buffer: Arc<RwLock<HashMap<String, MetricsBuffer>>>,
82}
83
84impl std::fmt::Debug for CanaryDeploymentEngine {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        f.debug_struct("CanaryDeploymentEngine")
87            .field("deployments", &self.deployments)
88            .field("health_monitor", &self.health_monitor.as_ref().map(|_| "Arc<dyn HealthMonitor>"))
89            .field("default_config", &self.default_config)
90            .field("metrics_buffer", &self.metrics_buffer)
91            .finish()
92    }
93}
94
95/// Buffer for storing metrics samples
96#[derive(Debug, Clone)]
97struct MetricsBuffer {
98    canary_latencies: VecDeque<f64>,
99    control_latencies: VecDeque<f64>,
100    canary_successes: u64,
101    canary_failures: u64,
102    control_successes: u64,
103    control_failures: u64,
104    max_samples: usize,
105}
106
107impl MetricsBuffer {
108    fn new(max_samples: usize) -> Self {
109        Self {
110            canary_latencies: VecDeque::with_capacity(max_samples),
111            control_latencies: VecDeque::with_capacity(max_samples),
112            canary_successes: 0,
113            canary_failures: 0,
114            control_successes: 0,
115            control_failures: 0,
116            max_samples,
117        }
118    }
119
120    fn add_canary_latency(&mut self, latency: f64) {
121        if self.canary_latencies.len() >= self.max_samples {
122            self.canary_latencies.pop_front();
123        }
124        self.canary_latencies.push_back(latency);
125    }
126
127    fn add_control_latency(&mut self, latency: f64) {
128        if self.control_latencies.len() >= self.max_samples {
129            self.control_latencies.pop_front();
130        }
131        self.control_latencies.push_back(latency);
132    }
133
134    fn record_canary_result(&mut self, success: bool) {
135        if success {
136            self.canary_successes += 1;
137        } else {
138            self.canary_failures += 1;
139        }
140    }
141
142    fn record_control_result(&mut self, success: bool) {
143        if success {
144            self.control_successes += 1;
145        } else {
146            self.control_failures += 1;
147        }
148    }
149
150    fn calculate_percentile(values: &VecDeque<f64>, percentile: f64) -> f64 {
151        if values.is_empty() {
152            return 0.0;
153        }
154
155        let mut sorted: Vec<f64> = values.iter().copied().collect();
156        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
157
158        let index = ((percentile / 100.0) * (sorted.len() - 1) as f64).round() as usize;
159        sorted[index]
160    }
161
162    fn get_metrics(&self) -> (f64, f64, f64, f64, f64, f64) {
163        let canary_p95 = Self::calculate_percentile(&self.canary_latencies, 95.0);
164        let canary_p99 = Self::calculate_percentile(&self.canary_latencies, 99.0);
165        let control_p95 = Self::calculate_percentile(&self.control_latencies, 95.0);
166        let control_p99 = Self::calculate_percentile(&self.control_latencies, 99.0);
167
168        let canary_total = self.canary_successes + self.canary_failures;
169        let control_total = self.control_successes + self.control_failures;
170
171        let canary_success_rate = if canary_total > 0 {
172            (self.canary_successes as f64 / canary_total as f64) * 100.0
173        } else {
174            0.0
175        };
176
177        let control_success_rate = if control_total > 0 {
178            (self.control_successes as f64 / control_total as f64) * 100.0
179        } else {
180            0.0
181        };
182
183        (
184            canary_success_rate,
185            control_success_rate,
186            canary_p95,
187            control_p95,
188            canary_p99,
189            control_p99,
190        )
191    }
192}
193
194impl CanaryDeploymentEngine {
195    /// Create a new canary deployment engine
196    pub fn new(default_config: CanaryConfig) -> Self {
197        Self {
198            deployments: Arc::new(RwLock::new(HashMap::new())),
199            health_monitor: None,
200            default_config,
201            metrics_buffer: Arc::new(RwLock::new(HashMap::new())),
202        }
203    }
204
205    /// Create with custom health monitor
206    pub fn with_health_monitor(
207        default_config: CanaryConfig,
208        health_monitor: Arc<dyn HealthMonitor>,
209    ) -> Self {
210        Self {
211            deployments: Arc::new(RwLock::new(HashMap::new())),
212            health_monitor: Some(health_monitor),
213            default_config,
214            metrics_buffer: Arc::new(RwLock::new(HashMap::new())),
215        }
216    }
217
218    /// Check if deployment should be auto-promoted
219    async fn should_auto_promote(&self, deployment_id: &str) -> ActuatorResult<bool> {
220        let deployments = self.deployments.read().await;
221        let state = deployments
222            .get(deployment_id)
223            .ok_or_else(|| ActuatorError::DeploymentFailed("Deployment not found".to_string()))?;
224
225        if !state.config.auto_promote {
226            return Ok(false);
227        }
228
229        // Check if phase duration has elapsed
230        let now = Utc::now();
231        let phase_duration = state.config.phase_duration;
232        let elapsed = now
233            .signed_duration_since(state.phase_start_time)
234            .to_std()
235            .unwrap_or_default();
236
237        if elapsed < phase_duration {
238            debug!(
239                "Phase duration not elapsed: {:?}/{:?}",
240                elapsed, phase_duration
241            );
242            return Ok(false);
243        }
244
245        // Check success criteria
246        self.validate_success_criteria(deployment_id).await
247    }
248
249    /// Validate success criteria
250    async fn validate_success_criteria(&self, deployment_id: &str) -> ActuatorResult<bool> {
251        let deployments = self.deployments.read().await;
252        let state = deployments
253            .get(deployment_id)
254            .ok_or_else(|| ActuatorError::DeploymentFailed("Deployment not found".to_string()))?;
255
256        let criteria = &state.config.success_criteria;
257        let metrics = &state.metrics;
258
259        // Check minimum sample size
260        if (metrics.canary_requests as usize) < criteria.min_sample_size {
261            debug!(
262                "Insufficient sample size: {} < {}",
263                metrics.canary_requests, criteria.min_sample_size
264            );
265            return Ok(false);
266        }
267
268        // Check success rate
269        if metrics.canary_success_rate < criteria.min_success_rate {
270            warn!(
271                "Canary success rate {} below threshold {}",
272                metrics.canary_success_rate, criteria.min_success_rate
273            );
274            return Ok(false);
275        }
276
277        // Check error rate
278        if metrics.canary_error_rate > criteria.max_error_rate {
279            warn!(
280                "Canary error rate {} exceeds threshold {}",
281                metrics.canary_error_rate, criteria.max_error_rate
282            );
283            return Ok(false);
284        }
285
286        // Check P95 latency
287        if metrics.canary_p95_latency_ms > criteria.max_p95_latency_ms {
288            warn!(
289                "Canary P95 latency {} exceeds threshold {}",
290                metrics.canary_p95_latency_ms, criteria.max_p95_latency_ms
291            );
292            return Ok(false);
293        }
294
295        // Check P99 latency
296        if metrics.canary_p99_latency_ms > criteria.max_p99_latency_ms {
297            warn!(
298                "Canary P99 latency {} exceeds threshold {}",
299                metrics.canary_p99_latency_ms, criteria.max_p99_latency_ms
300            );
301            return Ok(false);
302        }
303
304        // Check cost increase
305        if metrics.canary_cost_per_request > 0.0 && metrics.control_cost_per_request > 0.0 {
306            let cost_increase = ((metrics.canary_cost_per_request
307                - metrics.control_cost_per_request)
308                / metrics.control_cost_per_request)
309                * 100.0;
310            if cost_increase > criteria.max_cost_increase_pct {
311                warn!(
312                    "Cost increase {}% exceeds threshold {}%",
313                    cost_increase, criteria.max_cost_increase_pct
314                );
315                return Ok(false);
316            }
317        }
318
319        // Check statistical significance if available
320        if let Some(analysis) = &metrics.statistical_analysis {
321            if analysis.significant_difference && analysis.success_rate_p_value < 0.05 {
322                // Significant difference detected - check if it's positive
323                if metrics.canary_success_rate < metrics.control_success_rate {
324                    warn!("Statistically significant degradation detected");
325                    return Ok(false);
326                }
327            }
328        }
329
330        info!("All success criteria met for deployment {}", deployment_id);
331        Ok(true)
332    }
333
334    /// Check if deployment should be auto-rolled back
335    async fn should_auto_rollback(
336        &self,
337        deployment_id: &str,
338    ) -> ActuatorResult<Option<RollbackReason>> {
339        let deployments = self.deployments.read().await;
340        let state = deployments
341            .get(deployment_id)
342            .ok_or_else(|| ActuatorError::DeploymentFailed("Deployment not found".to_string()))?;
343
344        if !state.config.auto_rollback {
345            return Ok(None);
346        }
347
348        let criteria = &state.config.success_criteria;
349        let metrics = &state.metrics;
350
351        // Need minimum sample size before making rollback decisions
352        if (metrics.canary_requests as usize) < criteria.min_sample_size {
353            return Ok(None);
354        }
355
356        // Check critical failures
357        if metrics.canary_error_rate > criteria.max_error_rate * 2.0 {
358            return Ok(Some(RollbackReason::HighErrorRate));
359        }
360
361        if metrics.canary_p99_latency_ms > criteria.max_p99_latency_ms * 2.0 {
362            return Ok(Some(RollbackReason::HighLatency));
363        }
364
365        // Check health status
366        if !state.health.healthy && state.health.checks_failed > 2 {
367            return Ok(Some(RollbackReason::HealthCheckFailed));
368        }
369
370        // Check timeout
371        let now = Utc::now();
372        let total_elapsed = now
373            .signed_duration_since(state.deployment_start_time)
374            .to_std()
375            .unwrap_or_default();
376        if total_elapsed > state.config.max_duration {
377            return Ok(Some(RollbackReason::Timeout));
378        }
379
380        Ok(None)
381    }
382
383    /// Update deployment metrics
384    pub async fn update_metrics(
385        &self,
386        deployment_id: &str,
387        is_canary: bool,
388        latency_ms: f64,
389        success: bool,
390        cost: Option<f64>,
391    ) -> ActuatorResult<()> {
392        // Update metrics buffer
393        {
394            let mut buffers = self.metrics_buffer.write().await;
395            let buffer = buffers
396                .entry(deployment_id.to_string())
397                .or_insert_with(|| MetricsBuffer::new(10000));
398
399            if is_canary {
400                buffer.add_canary_latency(latency_ms);
401                buffer.record_canary_result(success);
402            } else {
403                buffer.add_control_latency(latency_ms);
404                buffer.record_control_result(success);
405            }
406        }
407
408        // Update deployment metrics
409        let mut deployments = self.deployments.write().await;
410        let state = deployments.get_mut(deployment_id).ok_or_else(|| {
411            ActuatorError::DeploymentFailed("Deployment not found".to_string())
412        })?;
413
414        state.metrics.total_requests += 1;
415
416        if is_canary {
417            state.metrics.canary_requests += 1;
418            if let Some(cost_value) = cost {
419                // Update running average
420                let n = state.metrics.canary_requests as f64;
421                state.metrics.canary_cost_per_request =
422                    ((n - 1.0) * state.metrics.canary_cost_per_request + cost_value) / n;
423            }
424        } else {
425            state.metrics.control_requests += 1;
426            if let Some(cost_value) = cost {
427                let n = state.metrics.control_requests as f64;
428                state.metrics.control_cost_per_request =
429                    ((n - 1.0) * state.metrics.control_cost_per_request + cost_value) / n;
430            }
431        }
432
433        // Recalculate metrics from buffer
434        let buffers = self.metrics_buffer.read().await;
435        if let Some(buffer) = buffers.get(deployment_id) {
436            let (canary_sr, control_sr, canary_p95, control_p95, canary_p99, control_p99) =
437                buffer.get_metrics();
438
439            state.metrics.canary_success_rate = canary_sr;
440            state.metrics.control_success_rate = control_sr;
441            state.metrics.canary_error_rate = 100.0 - canary_sr;
442            state.metrics.control_error_rate = 100.0 - control_sr;
443            state.metrics.canary_p95_latency_ms = canary_p95;
444            state.metrics.control_p95_latency_ms = control_p95;
445            state.metrics.canary_p99_latency_ms = canary_p99;
446            state.metrics.control_p99_latency_ms = control_p99;
447
448            // Perform statistical analysis
449            if buffer.canary_successes + buffer.canary_failures
450                >= state.config.success_criteria.min_sample_size as u64
451                && buffer.control_successes + buffer.control_failures
452                    >= state.config.success_criteria.min_sample_size as u64
453            {
454                state.metrics.statistical_analysis = Some(self.perform_statistical_analysis(buffer));
455            }
456        }
457
458        state.last_updated = Utc::now();
459
460        Ok(())
461    }
462
463    /// Perform statistical analysis on metrics
464    fn perform_statistical_analysis(&self, buffer: &MetricsBuffer) -> StatisticalAnalysis {
465        let canary_total = buffer.canary_successes + buffer.canary_failures;
466        let control_total = buffer.control_successes + buffer.control_failures;
467
468        // Chi-square test for success rates
469        let canary_success_rate =
470            buffer.canary_successes as f64 / canary_total.max(1) as f64;
471        let control_success_rate =
472            buffer.control_successes as f64 / control_total.max(1) as f64;
473
474        // Simplified p-value calculation (in production, use proper statistical library)
475        let pooled_rate = (buffer.canary_successes + buffer.control_successes) as f64
476            / (canary_total + control_total) as f64;
477        let se = (pooled_rate * (1.0 - pooled_rate)
478            * (1.0 / canary_total as f64 + 1.0 / control_total as f64))
479            .sqrt();
480
481        let z_score = (canary_success_rate - control_success_rate) / se.max(0.0001);
482        let success_rate_p_value = 2.0 * (1.0 - normal_cdf(z_score.abs()));
483
484        // T-test for latency (simplified)
485        let latency_p_value = if !buffer.canary_latencies.is_empty()
486            && !buffer.control_latencies.is_empty()
487        {
488            let canary_mean: f64 = buffer.canary_latencies.iter().sum::<f64>()
489                / buffer.canary_latencies.len() as f64;
490            let control_mean: f64 = buffer.control_latencies.iter().sum::<f64>()
491                / buffer.control_latencies.len() as f64;
492
493            let canary_var: f64 = buffer
494                .canary_latencies
495                .iter()
496                .map(|x| (x - canary_mean).powi(2))
497                .sum::<f64>()
498                / buffer.canary_latencies.len().max(1) as f64;
499
500            let control_var: f64 = buffer
501                .control_latencies
502                .iter()
503                .map(|x| (x - control_mean).powi(2))
504                .sum::<f64>()
505                / buffer.control_latencies.len().max(1) as f64;
506
507            let se_latency = (canary_var / buffer.canary_latencies.len() as f64
508                + control_var / buffer.control_latencies.len() as f64)
509                .sqrt();
510
511            let t_stat = (canary_mean - control_mean) / se_latency.max(0.0001);
512            2.0 * (1.0 - normal_cdf(t_stat.abs()))
513        } else {
514            1.0
515        };
516
517        let significant_difference = success_rate_p_value < 0.05 || latency_p_value < 0.05;
518
519        StatisticalAnalysis {
520            success_rate_p_value,
521            latency_p_value,
522            significant_difference,
523            confidence_level: 0.95,
524            canary_sample_size: canary_total as usize,
525            control_sample_size: control_total as usize,
526        }
527    }
528
529    /// Pause a deployment (manual control)
530    pub async fn pause(&self, deployment_id: &str) -> ActuatorResult<()> {
531        let mut deployments = self.deployments.write().await;
532        let state = deployments.get_mut(deployment_id).ok_or_else(|| {
533            ActuatorError::DeploymentFailed("Deployment not found".to_string())
534        })?;
535
536        state.paused = true;
537        state.last_updated = Utc::now();
538        info!("Deployment {} paused", deployment_id);
539        Ok(())
540    }
541
542    /// Resume a paused deployment
543    pub async fn resume(&self, deployment_id: &str) -> ActuatorResult<()> {
544        let mut deployments = self.deployments.write().await;
545        let state = deployments.get_mut(deployment_id).ok_or_else(|| {
546            ActuatorError::DeploymentFailed("Deployment not found".to_string())
547        })?;
548
549        state.paused = false;
550        state.phase_start_time = Utc::now(); // Reset phase timer
551        state.last_updated = Utc::now();
552        info!("Deployment {} resumed", deployment_id);
553        Ok(())
554    }
555
556    /// Check if deployment is paused
557    pub async fn is_paused(&self, deployment_id: &str) -> ActuatorResult<bool> {
558        let deployments = self.deployments.read().await;
559        let state = deployments
560            .get(deployment_id)
561            .ok_or_else(|| ActuatorError::DeploymentFailed("Deployment not found".to_string()))?;
562        Ok(state.paused)
563    }
564
565    /// Get deployment configuration
566    pub async fn get_config(&self, deployment_id: &str) -> ActuatorResult<CanaryConfig> {
567        let deployments = self.deployments.read().await;
568        let state = deployments
569            .get(deployment_id)
570            .ok_or_else(|| ActuatorError::DeploymentFailed("Deployment not found".to_string()))?;
571        Ok(state.config.clone())
572    }
573
574    /// Update health status
575    pub async fn update_health(
576        &self,
577        deployment_id: &str,
578        health: HealthStatus,
579    ) -> ActuatorResult<()> {
580        let mut deployments = self.deployments.write().await;
581        let state = deployments.get_mut(deployment_id).ok_or_else(|| {
582            ActuatorError::DeploymentFailed("Deployment not found".to_string())
583        })?;
584
585        state.health = health;
586        state.last_updated = Utc::now();
587        Ok(())
588    }
589}
590
591#[async_trait]
592impl CanaryDeployment for CanaryDeploymentEngine {
593    async fn start_canary(
594        &mut self,
595        request: DeploymentRequest,
596    ) -> ActuatorResult<DeploymentStatus> {
597        let deployment_id = request.id.clone();
598        info!("Starting canary deployment: {}", deployment_id);
599
600        // Validate request
601        if request.config_changes.is_empty() {
602            return Err(ActuatorError::ValidationFailed(
603                "No configuration changes specified".to_string(),
604            ));
605        }
606
607        // Initialize deployment state
608        let now = Utc::now();
609        let config = self.default_config.clone();
610
611        let initial_health = HealthStatus {
612            healthy: true,
613            checks_passed: 0,
614            checks_failed: 0,
615            check_results: vec![],
616            last_check_time: now,
617        };
618
619        let state = CanaryDeploymentState {
620            request: request.clone(),
621            config: config.clone(),
622            current_phase: 0,
623            phase_start_time: now,
624            deployment_start_time: now,
625            state: DeploymentState::Validating,
626            metrics: DeploymentMetrics::default(),
627            health: initial_health.clone(),
628            errors: vec![],
629            paused: false,
630            traffic_split_counter: 0,
631            last_updated: now,
632        };
633
634        // Store deployment state
635        {
636            let mut deployments = self.deployments.write().await;
637            deployments.insert(deployment_id.clone(), state);
638        }
639
640        // Initialize metrics buffer
641        {
642            let mut buffers = self.metrics_buffer.write().await;
643            buffers.insert(
644                deployment_id.clone(),
645                MetricsBuffer::new(10000),
646            );
647        }
648
649        // Transition to rolling out
650        {
651            let mut deployments = self.deployments.write().await;
652            if let Some(state) = deployments.get_mut(&deployment_id) {
653                state.state = DeploymentState::RollingOut;
654                state.last_updated = Utc::now();
655            }
656        }
657
658        info!(
659            "Canary deployment {} started with {} phases",
660            deployment_id,
661            config.phases.len()
662        );
663
664        self.get_status(&deployment_id).await
665    }
666
667    async fn promote(&mut self, deployment_id: &str) -> ActuatorResult<DeploymentStatus> {
668        info!("Promoting canary deployment: {}", deployment_id);
669
670        // Validate promotion
671        if !self.can_promote(deployment_id).await? {
672            return Err(ActuatorError::ValidationFailed(
673                "Cannot promote: success criteria not met".to_string(),
674            ));
675        }
676
677        let mut deployments = self.deployments.write().await;
678        let state = deployments.get_mut(deployment_id).ok_or_else(|| {
679            ActuatorError::DeploymentFailed("Deployment not found".to_string())
680        })?;
681
682        // Move to next phase
683        if state.current_phase < state.config.phases.len() - 1 {
684            state.current_phase += 1;
685            state.phase_start_time = Utc::now();
686            state.state = DeploymentState::RollingOut;
687            state.last_updated = Utc::now();
688
689            let traffic_percent = state.config.phases[state.current_phase].traffic_percent;
690            info!(
691                "Promoted deployment {} to phase {} ({}% traffic)",
692                deployment_id, state.current_phase, traffic_percent
693            );
694        } else {
695            // Already at final phase, mark as completed
696            state.state = DeploymentState::Completed;
697            state.last_updated = Utc::now();
698            info!("Deployment {} completed", deployment_id);
699        }
700
701        drop(deployments);
702        self.get_status(deployment_id).await
703    }
704
705    async fn complete(&mut self, deployment_id: &str) -> ActuatorResult<DeploymentStatus> {
706        info!("Completing canary deployment: {}", deployment_id);
707
708        let mut deployments = self.deployments.write().await;
709        let state = deployments.get_mut(deployment_id).ok_or_else(|| {
710            ActuatorError::DeploymentFailed("Deployment not found".to_string())
711        })?;
712
713        // Jump to 100% phase
714        state.current_phase = state.config.phases.len() - 1;
715        state.state = DeploymentState::Completed;
716        state.last_updated = Utc::now();
717
718        info!("Deployment {} completed at 100%", deployment_id);
719
720        drop(deployments);
721        self.get_status(deployment_id).await
722    }
723
724    async fn abort(&mut self, deployment_id: &str) -> ActuatorResult<RollbackResult> {
725        info!("Aborting canary deployment: {}", deployment_id);
726
727        let request = RollbackRequest {
728            deployment_id: deployment_id.to_string(),
729            reason: RollbackReason::Manual,
730            timestamp: Utc::now(),
731            automatic: false,
732            metadata: HashMap::new(),
733        };
734
735        self.execute_rollback(&request).await
736    }
737
738    async fn get_status(&self, deployment_id: &str) -> ActuatorResult<DeploymentStatus> {
739        let deployments = self.deployments.read().await;
740        let state = deployments
741            .get(deployment_id)
742            .ok_or_else(|| ActuatorError::DeploymentFailed("Deployment not found".to_string()))?;
743
744        let traffic_percent = if state.current_phase < state.config.phases.len() {
745            state.config.phases[state.current_phase].traffic_percent
746        } else {
747            100.0
748        };
749
750        Ok(DeploymentStatus {
751            deployment_id: deployment_id.to_string(),
752            state: state.state,
753            current_phase: Some(state.current_phase),
754            traffic_percent,
755            start_time: state.deployment_start_time,
756            phase_start_time: Some(state.phase_start_time),
757            health: state.health.clone(),
758            metrics: state.metrics.clone(),
759            errors: state.errors.clone(),
760            last_updated: state.last_updated,
761        })
762    }
763
764    async fn can_promote(&self, deployment_id: &str) -> ActuatorResult<bool> {
765        let deployments = self.deployments.read().await;
766        let state = deployments
767            .get(deployment_id)
768            .ok_or_else(|| ActuatorError::DeploymentFailed("Deployment not found".to_string()))?;
769
770        // Cannot promote if paused
771        if state.paused {
772            return Ok(false);
773        }
774
775        // Cannot promote if not in rolling out state
776        if state.state != DeploymentState::RollingOut {
777            return Ok(false);
778        }
779
780        // Cannot promote if already at final phase
781        if state.current_phase >= state.config.phases.len() - 1 {
782            return Ok(false);
783        }
784
785        drop(deployments);
786
787        // Check success criteria
788        self.validate_success_criteria(deployment_id).await
789    }
790}
791
792impl CanaryDeploymentEngine {
793    /// Execute rollback
794    async fn execute_rollback(&self, request: &RollbackRequest) -> ActuatorResult<RollbackResult> {
795        let start_time = Utc::now();
796        let deployment_id = &request.deployment_id;
797
798        let mut deployments = self.deployments.write().await;
799        let state = deployments.get_mut(deployment_id).ok_or_else(|| {
800            ActuatorError::RollbackFailed("Deployment not found".to_string())
801        })?;
802
803        // Update state
804        state.state = DeploymentState::RollingBack;
805        state.last_updated = Utc::now();
806
807        let reverted_changes = state.request.config_changes.clone();
808
809        // Mark as rolled back
810        state.state = DeploymentState::RolledBack;
811        state.last_updated = Utc::now();
812
813        let duration = Utc::now()
814            .signed_duration_since(start_time)
815            .to_std()
816            .unwrap_or_default();
817
818        info!(
819            "Rollback completed for deployment {} in {:?}",
820            deployment_id, duration
821        );
822
823        Ok(RollbackResult {
824            deployment_id: deployment_id.clone(),
825            success: true,
826            error: None,
827            reverted_changes,
828            timestamp: Utc::now(),
829            duration,
830        })
831    }
832}
833
834#[async_trait]
835impl TrafficSplitter for CanaryDeploymentEngine {
836    async fn route_request(&self, request_id: &str, canary_percent: f64) -> ActuatorResult<bool> {
837        // Get deployment ID from request (simplified - in production would be more complex)
838        let deployments = self.deployments.read().await;
839
840        // Find active deployment
841        let active_deployment = deployments
842            .values()
843            .find(|d| d.state == DeploymentState::RollingOut);
844
845        if let Some(deployment) = active_deployment {
846            match deployment.config.traffic_split_strategy {
847                TrafficSplitStrategy::WeightedRandom => {
848                    let mut rng = rand::thread_rng();
849                    let random: f64 = rng.gen();
850                    Ok(random < (canary_percent / 100.0))
851                }
852                TrafficSplitStrategy::ConsistentHash => {
853                    let mut hasher = std::collections::hash_map::DefaultHasher::new();
854                    request_id.hash(&mut hasher);
855                    let hash = hasher.finish();
856                    Ok((hash % 100) < canary_percent as u64)
857                }
858                TrafficSplitStrategy::RoundRobin => {
859                    // Round-robin based on counter (would need to increment counter)
860                    let counter = deployment.traffic_split_counter;
861                    Ok((counter % 100) < canary_percent as u64)
862                }
863                TrafficSplitStrategy::Geographic => {
864                    // Simplified: use hash of request_id
865                    let mut hasher = std::collections::hash_map::DefaultHasher::new();
866                    request_id.hash(&mut hasher);
867                    let hash = hasher.finish();
868                    Ok((hash % 100) < canary_percent as u64)
869                }
870            }
871        } else {
872            // No active deployment, route to control
873            Ok(false)
874        }
875    }
876
877    fn get_routing_stats(&self) -> HashMap<String, u64> {
878        // Would be implemented with actual routing statistics
879        HashMap::new()
880    }
881}
882
883/// Simplified normal CDF for statistical analysis
884fn normal_cdf(x: f64) -> f64 {
885    0.5 * (1.0 + erf(x / std::f64::consts::SQRT_2))
886}
887
888/// Error function approximation
889fn erf(x: f64) -> f64 {
890    let a1 = 0.254829592;
891    let a2 = -0.284496736;
892    let a3 = 1.421413741;
893    let a4 = -1.453152027;
894    let a5 = 1.061405429;
895    let p = 0.3275911;
896
897    let sign = if x < 0.0 { -1.0 } else { 1.0 };
898    let x = x.abs();
899
900    let t = 1.0 / (1.0 + p * x);
901    let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
902
903    sign * y
904}
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909    use std::time::Duration as StdDuration;
910
911    fn create_test_request(id: &str) -> DeploymentRequest {
912        DeploymentRequest {
913            id: id.to_string(),
914            timestamp: Utc::now(),
915            decision: crate::decision::Decision {
916                id: format!("decision-{}", id),
917                timestamp: Utc::now(),
918                decision_type: crate::decision::DecisionType::PromptOptimization,
919                criteria: crate::decision::DecisionCriteria {
920                    metric_name: "latency".to_string(),
921                    operator: crate::decision::ComparisonOperator::LessThan,
922                    threshold: 100.0,
923                    priority: 1.0,
924                },
925                config_changes: vec![],
926                expected_impact: crate::decision::ExpectedImpact::default(),
927                rollback_plan: crate::decision::RollbackPlan::default(),
928                safety_checks: vec![],
929                metadata: HashMap::new(),
930            },
931            strategy: super::super::types::DeploymentStrategy::Canary,
932            config_changes: vec![],
933            expected_duration: StdDuration::from_secs(300),
934            metadata: HashMap::new(),
935        }
936    }
937
938    #[tokio::test]
939    async fn test_start_canary_deployment() {
940        let config = CanaryConfig::default();
941        let mut engine = CanaryDeploymentEngine::new(config);
942
943        let mut request = create_test_request("test-deploy-1");
944        request.config_changes = vec![crate::decision::ConfigChange {
945            config_type: crate::decision::ConfigType::Provider,
946            key: "model".to_string(),
947            old_value: serde_json::json!("gpt-3.5-turbo"),
948            new_value: serde_json::json!("gpt-4"),
949            reason: "Testing".to_string(),
950        }];
951
952        let status = engine.start_canary(request).await.unwrap();
953
954        assert_eq!(status.deployment_id, "test-deploy-1");
955        assert_eq!(status.state, DeploymentState::RollingOut);
956        assert_eq!(status.current_phase, Some(0));
957        assert_eq!(status.traffic_percent, 1.0);
958    }
959
960    #[tokio::test]
961    async fn test_cannot_start_without_config_changes() {
962        let config = CanaryConfig::default();
963        let mut engine = CanaryDeploymentEngine::new(config);
964
965        let request = create_test_request("test-deploy-2");
966        let result = engine.start_canary(request).await;
967
968        assert!(result.is_err());
969        assert!(matches!(
970            result.unwrap_err(),
971            ActuatorError::ValidationFailed(_)
972        ));
973    }
974
975    #[tokio::test]
976    async fn test_update_metrics() {
977        let config = CanaryConfig::default();
978        let mut engine = CanaryDeploymentEngine::new(config);
979
980        let mut request = create_test_request("test-deploy-3");
981        request.config_changes = vec![crate::decision::ConfigChange {
982            config_type: crate::decision::ConfigType::Provider,
983            key: "model".to_string(),
984            old_value: serde_json::json!("gpt-3.5-turbo"),
985            new_value: serde_json::json!("gpt-4"),
986            reason: "Testing".to_string(),
987        }];
988
989        engine.start_canary(request).await.unwrap();
990
991        // Update some metrics
992        for i in 0..100 {
993            let is_canary = i % 2 == 0;
994            engine
995                .update_metrics("test-deploy-3", is_canary, 50.0, true, Some(0.001))
996                .await
997                .unwrap();
998        }
999
1000        let status = engine.get_status("test-deploy-3").await.unwrap();
1001        assert_eq!(status.metrics.total_requests, 100);
1002        assert_eq!(status.metrics.canary_requests, 50);
1003        assert_eq!(status.metrics.control_requests, 50);
1004    }
1005
1006    #[tokio::test]
1007    async fn test_traffic_splitting_weighted_random() {
1008        let config = CanaryConfig {
1009            traffic_split_strategy: TrafficSplitStrategy::WeightedRandom,
1010            ..Default::default()
1011        };
1012        let mut engine = CanaryDeploymentEngine::new(config);
1013
1014        let mut request = create_test_request("test-deploy-4");
1015        request.config_changes = vec![crate::decision::ConfigChange {
1016            config_type: crate::decision::ConfigType::Provider,
1017            key: "model".to_string(),
1018            old_value: serde_json::json!("gpt-3.5-turbo"),
1019            new_value: serde_json::json!("gpt-4"),
1020            reason: "Testing".to_string(),
1021        }];
1022
1023        engine.start_canary(request).await.unwrap();
1024
1025        // Test routing with 50% traffic
1026        let mut canary_count = 0;
1027        let iterations = 1000;
1028
1029        for i in 0..iterations {
1030            let route_to_canary = engine
1031                .route_request(&format!("req-{}", i), 50.0)
1032                .await
1033                .unwrap();
1034            if route_to_canary {
1035                canary_count += 1;
1036            }
1037        }
1038
1039        // Should be approximately 50% (allow 10% variance)
1040        let canary_percent = (canary_count as f64 / iterations as f64) * 100.0;
1041        assert!(canary_percent > 40.0 && canary_percent < 60.0);
1042    }
1043
1044    #[tokio::test]
1045    async fn test_traffic_splitting_consistent_hash() {
1046        let config = CanaryConfig {
1047            traffic_split_strategy: TrafficSplitStrategy::ConsistentHash,
1048            ..Default::default()
1049        };
1050        let mut engine = CanaryDeploymentEngine::new(config);
1051
1052        let mut request = create_test_request("test-deploy-5");
1053        request.config_changes = vec![crate::decision::ConfigChange {
1054            config_type: crate::decision::ConfigType::Provider,
1055            key: "model".to_string(),
1056            old_value: serde_json::json!("gpt-3.5-turbo"),
1057            new_value: serde_json::json!("gpt-4"),
1058            reason: "Testing".to_string(),
1059        }];
1060
1061        engine.start_canary(request).await.unwrap();
1062
1063        // Same request should always route the same way
1064        let request_id = "consistent-request-123";
1065        let route1 = engine.route_request(request_id, 50.0).await.unwrap();
1066        let route2 = engine.route_request(request_id, 50.0).await.unwrap();
1067        let route3 = engine.route_request(request_id, 50.0).await.unwrap();
1068
1069        assert_eq!(route1, route2);
1070        assert_eq!(route2, route3);
1071    }
1072
1073    #[tokio::test]
1074    async fn test_pause_and_resume() {
1075        let config = CanaryConfig::default();
1076        let mut engine = CanaryDeploymentEngine::new(config);
1077
1078        let mut request = create_test_request("test-deploy-6");
1079        request.config_changes = vec![crate::decision::ConfigChange {
1080            config_type: crate::decision::ConfigType::Provider,
1081            key: "model".to_string(),
1082            old_value: serde_json::json!("gpt-3.5-turbo"),
1083            new_value: serde_json::json!("gpt-4"),
1084            reason: "Testing".to_string(),
1085        }];
1086
1087        engine.start_canary(request).await.unwrap();
1088
1089        // Pause
1090        engine.pause("test-deploy-6").await.unwrap();
1091        assert!(engine.is_paused("test-deploy-6").await.unwrap());
1092
1093        // Cannot promote when paused
1094        let can_promote = engine.can_promote("test-deploy-6").await.unwrap();
1095        assert!(!can_promote);
1096
1097        // Resume
1098        engine.resume("test-deploy-6").await.unwrap();
1099        assert!(!engine.is_paused("test-deploy-6").await.unwrap());
1100    }
1101
1102    #[tokio::test]
1103    async fn test_validation_with_insufficient_samples() {
1104        let config = CanaryConfig::default();
1105        let mut engine = CanaryDeploymentEngine::new(config);
1106
1107        let mut request = create_test_request("test-deploy-7");
1108        request.config_changes = vec![crate::decision::ConfigChange {
1109            config_type: crate::decision::ConfigType::Provider,
1110            key: "model".to_string(),
1111            old_value: serde_json::json!("gpt-3.5-turbo"),
1112            new_value: serde_json::json!("gpt-4"),
1113            reason: "Testing".to_string(),
1114        }];
1115
1116        engine.start_canary(request).await.unwrap();
1117
1118        // Add only a few samples (less than min_sample_size)
1119        for i in 0..10 {
1120            engine
1121                .update_metrics("test-deploy-7", true, 50.0, true, Some(0.001))
1122                .await
1123                .unwrap();
1124        }
1125
1126        // Should not be able to promote yet
1127        let can_promote = engine.can_promote("test-deploy-7").await.unwrap();
1128        assert!(!can_promote);
1129    }
1130
1131    #[tokio::test]
1132    async fn test_validation_with_sufficient_samples() {
1133        let mut config = CanaryConfig::default();
1134        config.success_criteria.min_sample_size = 50;
1135        config.phase_duration = StdDuration::from_millis(1); // Very short for testing
1136
1137        let mut engine = CanaryDeploymentEngine::new(config);
1138
1139        let mut request = create_test_request("test-deploy-8");
1140        request.config_changes = vec![crate::decision::ConfigChange {
1141            config_type: crate::decision::ConfigType::Provider,
1142            key: "model".to_string(),
1143            old_value: serde_json::json!("gpt-3.5-turbo"),
1144            new_value: serde_json::json!("gpt-4"),
1145            reason: "Testing".to_string(),
1146        }];
1147
1148        engine.start_canary(request).await.unwrap();
1149
1150        // Add sufficient samples with good metrics
1151        for _ in 0..100 {
1152            engine
1153                .update_metrics("test-deploy-8", true, 50.0, true, Some(0.001))
1154                .await
1155                .unwrap();
1156        }
1157
1158        // Wait for phase duration
1159        tokio::time::sleep(StdDuration::from_millis(10)).await;
1160
1161        // Should be able to promote now
1162        let can_promote = engine.can_promote("test-deploy-8").await.unwrap();
1163        assert!(can_promote);
1164    }
1165
1166    #[tokio::test]
1167    async fn test_high_error_rate_prevents_promotion() {
1168        let mut config = CanaryConfig::default();
1169        config.success_criteria.min_sample_size = 50;
1170        config.success_criteria.max_error_rate = 5.0;
1171        config.phase_duration = StdDuration::from_millis(1);
1172
1173        let mut engine = CanaryDeploymentEngine::new(config);
1174
1175        let mut request = create_test_request("test-deploy-9");
1176        request.config_changes = vec![crate::decision::ConfigChange {
1177            config_type: crate::decision::ConfigType::Provider,
1178            key: "model".to_string(),
1179            old_value: serde_json::json!("gpt-3.5-turbo"),
1180            new_value: serde_json::json!("gpt-4"),
1181            reason: "Testing".to_string(),
1182        }];
1183
1184        engine.start_canary(request).await.unwrap();
1185
1186        // Add samples with high error rate
1187        for i in 0..100 {
1188            let success = i % 10 != 0; // 10% error rate
1189            engine
1190                .update_metrics("test-deploy-9", true, 50.0, success, Some(0.001))
1191                .await
1192                .unwrap();
1193        }
1194
1195        tokio::time::sleep(StdDuration::from_millis(10)).await;
1196
1197        // Should not be able to promote due to high error rate
1198        let can_promote = engine.can_promote("test-deploy-9").await.unwrap();
1199        assert!(!can_promote);
1200    }
1201
1202    #[tokio::test]
1203    async fn test_promote_through_phases() {
1204        let mut config = CanaryConfig::default();
1205        config.success_criteria.min_sample_size = 10;
1206        config.phase_duration = StdDuration::from_millis(1);
1207
1208        let mut engine = CanaryDeploymentEngine::new(config);
1209
1210        let mut request = create_test_request("test-deploy-10");
1211        request.config_changes = vec![crate::decision::ConfigChange {
1212            config_type: crate::decision::ConfigType::Provider,
1213            key: "model".to_string(),
1214            old_value: serde_json::json!("gpt-3.5-turbo"),
1215            new_value: serde_json::json!("gpt-4"),
1216            reason: "Testing".to_string(),
1217        }];
1218
1219        engine.start_canary(request).await.unwrap();
1220
1221        // Initial phase
1222        let status = engine.get_status("test-deploy-10").await.unwrap();
1223        assert_eq!(status.current_phase, Some(0));
1224        assert_eq!(status.traffic_percent, 1.0);
1225
1226        // Add good metrics and promote
1227        for _ in 0..20 {
1228            engine
1229                .update_metrics("test-deploy-10", true, 50.0, true, Some(0.001))
1230                .await
1231                .unwrap();
1232        }
1233
1234        tokio::time::sleep(StdDuration::from_millis(10)).await;
1235
1236        // Promote to phase 1
1237        let status = engine.promote("test-deploy-10").await.unwrap();
1238        assert_eq!(status.current_phase, Some(1));
1239        assert_eq!(status.traffic_percent, 5.0);
1240    }
1241
1242    #[tokio::test]
1243    async fn test_complete_deployment() {
1244        let config = CanaryConfig::default();
1245        let mut engine = CanaryDeploymentEngine::new(config);
1246
1247        let mut request = create_test_request("test-deploy-11");
1248        request.config_changes = vec![crate::decision::ConfigChange {
1249            config_type: crate::decision::ConfigType::Provider,
1250            key: "model".to_string(),
1251            old_value: serde_json::json!("gpt-3.5-turbo"),
1252            new_value: serde_json::json!("gpt-4"),
1253            reason: "Testing".to_string(),
1254        }];
1255
1256        engine.start_canary(request).await.unwrap();
1257
1258        // Complete immediately
1259        let status = engine.complete("test-deploy-11").await.unwrap();
1260        assert_eq!(status.state, DeploymentState::Completed);
1261        assert_eq!(status.traffic_percent, 100.0);
1262    }
1263
1264    #[tokio::test]
1265    async fn test_abort_deployment() {
1266        let config = CanaryConfig::default();
1267        let mut engine = CanaryDeploymentEngine::new(config);
1268
1269        let mut request = create_test_request("test-deploy-12");
1270        request.config_changes = vec![crate::decision::ConfigChange {
1271            config_type: crate::decision::ConfigType::Provider,
1272            key: "model".to_string(),
1273            old_value: serde_json::json!("gpt-3.5-turbo"),
1274            new_value: serde_json::json!("gpt-4"),
1275            reason: "Testing".to_string(),
1276        }];
1277
1278        engine.start_canary(request).await.unwrap();
1279
1280        // Abort
1281        let result = engine.abort("test-deploy-12").await.unwrap();
1282        assert!(result.success);
1283        assert_eq!(result.deployment_id, "test-deploy-12");
1284
1285        // Check state
1286        let status = engine.get_status("test-deploy-12").await.unwrap();
1287        assert_eq!(status.state, DeploymentState::RolledBack);
1288    }
1289
1290    #[tokio::test]
1291    async fn test_metrics_buffer_percentile_calculation() {
1292        let mut buffer = MetricsBuffer::new(1000);
1293
1294        // Add latency samples
1295        for i in 1..=100 {
1296            buffer.add_canary_latency(i as f64);
1297        }
1298
1299        let (_, _, canary_p95, _, canary_p99, _) = buffer.get_metrics();
1300
1301        // P95 should be around 95
1302        assert!(canary_p95 >= 94.0 && canary_p95 <= 96.0);
1303
1304        // P99 should be around 99
1305        assert!(canary_p99 >= 98.0 && canary_p99 <= 100.0);
1306    }
1307
1308    #[tokio::test]
1309    async fn test_statistical_analysis() {
1310        let mut buffer = MetricsBuffer::new(1000);
1311
1312        // Add samples for both canary and control
1313        for _ in 0..200 {
1314            buffer.add_canary_latency(50.0);
1315            buffer.add_control_latency(50.0);
1316            buffer.record_canary_result(true);
1317            buffer.record_control_result(true);
1318        }
1319
1320        let config = CanaryConfig::default();
1321        let engine = CanaryDeploymentEngine::new(config);
1322        let analysis = engine.perform_statistical_analysis(&buffer);
1323
1324        // With identical distributions, should not find significant difference
1325        assert!(!analysis.significant_difference);
1326        assert_eq!(analysis.canary_sample_size, 200);
1327        assert_eq!(analysis.control_sample_size, 200);
1328    }
1329}