1use 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#[derive(Debug)]
30struct CanaryDeploymentState {
31 request: DeploymentRequest,
33
34 config: CanaryConfig,
36
37 current_phase: usize,
39
40 phase_start_time: DateTime<Utc>,
42
43 deployment_start_time: DateTime<Utc>,
45
46 state: DeploymentState,
48
49 metrics: DeploymentMetrics,
51
52 health: HealthStatus,
54
55 errors: Vec<String>,
57
58 paused: bool,
60
61 traffic_split_counter: u64,
63
64 last_updated: DateTime<Utc>,
66}
67
68#[derive(Clone)]
70pub struct CanaryDeploymentEngine {
71 deployments: Arc<RwLock<HashMap<String, CanaryDeploymentState>>>,
73
74 health_monitor: Option<Arc<dyn HealthMonitor>>,
76
77 default_config: CanaryConfig,
79
80 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#[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 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 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 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 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 self.validate_success_criteria(deployment_id).await
247 }
248
249 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 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 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 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 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 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 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 if let Some(analysis) = &metrics.statistical_analysis {
321 if analysis.significant_difference && analysis.success_rate_p_value < 0.05 {
322 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 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 if (metrics.canary_requests as usize) < criteria.min_sample_size {
353 return Ok(None);
354 }
355
356 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 if !state.health.healthy && state.health.checks_failed > 2 {
367 return Ok(Some(RollbackReason::HealthCheckFailed));
368 }
369
370 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 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 {
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 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 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 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 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 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 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 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 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 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 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(); state.last_updated = Utc::now();
552 info!("Deployment {} resumed", deployment_id);
553 Ok(())
554 }
555
556 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 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 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 if request.config_changes.is_empty() {
602 return Err(ActuatorError::ValidationFailed(
603 "No configuration changes specified".to_string(),
604 ));
605 }
606
607 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 {
636 let mut deployments = self.deployments.write().await;
637 deployments.insert(deployment_id.clone(), state);
638 }
639
640 {
642 let mut buffers = self.metrics_buffer.write().await;
643 buffers.insert(
644 deployment_id.clone(),
645 MetricsBuffer::new(10000),
646 );
647 }
648
649 {
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 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 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 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 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 if state.paused {
772 return Ok(false);
773 }
774
775 if state.state != DeploymentState::RollingOut {
777 return Ok(false);
778 }
779
780 if state.current_phase >= state.config.phases.len() - 1 {
782 return Ok(false);
783 }
784
785 drop(deployments);
786
787 self.validate_success_criteria(deployment_id).await
789 }
790}
791
792impl CanaryDeploymentEngine {
793 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 state.state = DeploymentState::RollingBack;
805 state.last_updated = Utc::now();
806
807 let reverted_changes = state.request.config_changes.clone();
808
809 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 let deployments = self.deployments.read().await;
839
840 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 let counter = deployment.traffic_split_counter;
861 Ok((counter % 100) < canary_percent as u64)
862 }
863 TrafficSplitStrategy::Geographic => {
864 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 Ok(false)
874 }
875 }
876
877 fn get_routing_stats(&self) -> HashMap<String, u64> {
878 HashMap::new()
880 }
881}
882
883fn normal_cdf(x: f64) -> f64 {
885 0.5 * (1.0 + erf(x / std::f64::consts::SQRT_2))
886}
887
888fn 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 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 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 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 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 engine.pause("test-deploy-6").await.unwrap();
1091 assert!(engine.is_paused("test-deploy-6").await.unwrap());
1092
1093 let can_promote = engine.can_promote("test-deploy-6").await.unwrap();
1095 assert!(!can_promote);
1096
1097 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 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 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); 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 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 tokio::time::sleep(StdDuration::from_millis(10)).await;
1160
1161 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 for i in 0..100 {
1188 let success = i % 10 != 0; 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 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 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 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 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 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 let result = engine.abort("test-deploy-12").await.unwrap();
1282 assert!(result.success);
1283 assert_eq!(result.deployment_id, "test-deploy-12");
1284
1285 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 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 assert!(canary_p95 >= 94.0 && canary_p95 <= 96.0);
1303
1304 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 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 assert!(!analysis.significant_difference);
1326 assert_eq!(analysis.canary_sample_size, 200);
1327 assert_eq!(analysis.control_sample_size, 200);
1328 }
1329}