use crate::calibration::{
CalibrationResult, CalibrationSample, Phase4Config,
sla_tripwires::{SlaTripwires, SlaConfig, AutoRevertAction, RevertEvent},
drift_monitor::{DriftMonitor, DriftThresholds, HealthStatus, AlertEvent},
shared_binning_core::{SharedBinningCore, SharedBinningConfig},
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, hash_map::DefaultHasher};
use std::hash::{Hash, Hasher};
use std::sync::{Arc, RwLock, atomic::{AtomicU64, AtomicBool, Ordering}};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use chrono::{DateTime, Utc};
use anyhow::{Result, Context as AnyhowContext};
use tracing::{info, warn, error};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalibV22Config {
pub enabled: bool,
pub rollout_percentage: u8,
pub rollout_stage: String,
pub bucket_strategy: BucketStrategy,
pub sla_gates: SlaGateConfig,
pub auto_revert_config: AutoRevertConfig,
pub config_fingerprint: String,
pub rollout_start_time: DateTime<Utc>,
pub promotion_criteria: PromotionCriteria,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BucketStrategy {
pub method: BucketMethod,
pub bucket_salt: String,
pub sticky_sessions: bool,
pub override_buckets: HashMap<String, bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BucketMethod {
RepositoryHash { salt: String },
Random,
Manual { assignments: HashMap<String, bool> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlaGateConfig {
pub max_p99_latency_increase_us: f64,
pub max_aece_tau_threshold: f64,
pub max_confidence_shift: f64,
pub require_zero_sla_recall_change: bool,
pub evaluation_window_minutes: u32,
pub consecutive_breach_threshold: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoRevertConfig {
pub enabled: bool,
pub breach_window_threshold: u32,
pub breach_window_duration_minutes: u32,
pub revert_cooldown_minutes: u32,
pub max_reverts_per_day: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromotionCriteria {
pub min_observation_hours: u32,
pub required_health_status: HealthStatus,
pub max_aece_degradation: f64,
pub require_p99_compliance: bool,
pub min_success_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RolloutStage {
Canary,
Limited,
Major,
Full,
Disabled,
}
impl RolloutStage {
fn get_percentage(&self) -> u8 {
match self {
RolloutStage::Canary => 5,
RolloutStage::Limited => 25,
RolloutStage::Major => 50,
RolloutStage::Full => 100,
RolloutStage::Disabled => 0,
}
}
fn next_stage(&self) -> Option<RolloutStage> {
match self {
RolloutStage::Canary => Some(RolloutStage::Limited),
RolloutStage::Limited => Some(RolloutStage::Major),
RolloutStage::Major => Some(RolloutStage::Full),
RolloutStage::Full => None,
RolloutStage::Disabled => Some(RolloutStage::Canary),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeatureFlagDecision {
pub use_calib_v22: bool,
pub decision_reason: String,
pub bucket_hash: u64,
pub rollout_stage: String,
pub config_fingerprint: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StageTransition {
pub timestamp: DateTime<Utc>,
pub from_stage: String,
pub to_stage: String,
pub trigger_reason: String,
pub metrics_snapshot: StageMetrics,
pub config_fingerprint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StageMetrics {
pub aece_value: f64,
pub p99_latency_us: f64,
pub success_rate: f64,
pub confidence_shift: f64,
pub health_status: HealthStatus,
pub observation_hours: f64,
pub sample_count: u64,
}
pub struct CalibV22FeatureFlag {
config: Arc<RwLock<CalibV22Config>>,
sla_tripwires: Arc<RwLock<SlaTripwires>>,
drift_monitor: Arc<RwLock<DriftMonitor>>,
current_stage: Arc<RwLock<RolloutStage>>,
stage_start_time: Arc<RwLock<Instant>>,
transition_history: Arc<RwLock<Vec<StageTransition>>>,
v22_success_count: Arc<AtomicU64>,
v22_failure_count: Arc<AtomicU64>,
control_success_count: Arc<AtomicU64>,
control_failure_count: Arc<AtomicU64>,
circuit_breaker_open: Arc<AtomicBool>,
last_revert_time: Arc<RwLock<Option<Instant>>>,
daily_revert_count: Arc<AtomicU64>,
recent_decisions: Arc<RwLock<Vec<FeatureFlagDecision>>>,
}
impl CalibV22FeatureFlag {
pub fn new(
config: CalibV22Config,
sla_config: SlaConfig,
drift_thresholds: DriftThresholds,
binning_config: SharedBinningConfig,
) -> Result<Self> {
if config.rollout_percentage > 100 {
anyhow::bail!("Invalid rollout percentage: {}", config.rollout_percentage);
}
let sla_tripwires = Arc::new(RwLock::new(
SlaTripwires::new(sla_config, drift_thresholds.clone())
));
let canary_gate_config = crate::calibration::drift_monitor::CanaryGateConfig::default();
let bootstrap_config = crate::calibration::fast_bootstrap::FastBootstrapConfig::default();
let drift_monitor = Arc::new(RwLock::new(
DriftMonitor::new(drift_thresholds, canary_gate_config, binning_config, bootstrap_config)
));
let current_stage = Self::stage_from_percentage(config.rollout_percentage);
info!("🚀 Initializing CALIB_V22 feature flag system");
info!("Initial stage: {:?} ({}%)", current_stage, config.rollout_percentage);
info!("Config fingerprint: {}", config.config_fingerprint);
Ok(Self {
config: Arc::new(RwLock::new(config)),
sla_tripwires,
drift_monitor,
current_stage: Arc::new(RwLock::new(current_stage)),
stage_start_time: Arc::new(RwLock::new(Instant::now())),
transition_history: Arc::new(RwLock::new(Vec::new())),
v22_success_count: Arc::new(AtomicU64::new(0)),
v22_failure_count: Arc::new(AtomicU64::new(0)),
control_success_count: Arc::new(AtomicU64::new(0)),
control_failure_count: Arc::new(AtomicU64::new(0)),
circuit_breaker_open: Arc::new(AtomicBool::new(false)),
last_revert_time: Arc::new(RwLock::new(None)),
daily_revert_count: Arc::new(AtomicU64::new(0)),
recent_decisions: Arc::new(RwLock::new(Vec::new())),
})
}
pub fn should_use_calib_v22(&self, repository_id: &str) -> Result<FeatureFlagDecision> {
let config = self.config.read().unwrap();
let current_stage = self.current_stage.read().unwrap();
let now = Utc::now();
if !config.enabled {
let decision = FeatureFlagDecision {
use_calib_v22: false,
decision_reason: "Feature flag globally disabled".to_string(),
bucket_hash: 0,
rollout_stage: format!("{:?}", *current_stage),
config_fingerprint: config.config_fingerprint.clone(),
timestamp: now,
};
self.record_decision(&decision)?;
return Ok(decision);
}
if self.circuit_breaker_open.load(Ordering::Relaxed) {
let decision = FeatureFlagDecision {
use_calib_v22: false,
decision_reason: "Circuit breaker open - auto-reverted".to_string(),
bucket_hash: 0,
rollout_stage: "Disabled".to_string(),
config_fingerprint: config.config_fingerprint.clone(),
timestamp: now,
};
self.record_decision(&decision)?;
return Ok(decision);
}
let bucket_hash = self.compute_repository_bucket(repository_id, &config.bucket_strategy)?;
if let Some(&override_enabled) = config.bucket_strategy.override_buckets.get(repository_id) {
let decision = FeatureFlagDecision {
use_calib_v22: override_enabled,
decision_reason: format!("Bucket override: {}", override_enabled),
bucket_hash,
rollout_stage: format!("{:?}", *current_stage),
config_fingerprint: config.config_fingerprint.clone(),
timestamp: now,
};
self.record_decision(&decision)?;
return Ok(decision);
}
let rollout_threshold = (config.rollout_percentage as f64 / 100.0 * u64::MAX as f64) as u64;
let use_v22 = bucket_hash <= rollout_threshold;
let decision = FeatureFlagDecision {
use_calib_v22: use_v22,
decision_reason: format!("Bucket assignment: hash={}, threshold={}, rollout={}%",
bucket_hash, rollout_threshold, config.rollout_percentage),
bucket_hash,
rollout_stage: format!("{:?}", *current_stage),
config_fingerprint: config.config_fingerprint.clone(),
timestamp: now,
};
self.record_decision(&decision)?;
Ok(decision)
}
pub fn record_calibration_result(&self,
use_v22: bool,
result: &CalibrationResult,
repository_id: &str
) -> Result<()> {
if result.calibrated_score >= 0.0 && result.calibrated_score <= 1.0 && !result.calibrated_score.is_nan() {
if use_v22 {
self.v22_success_count.fetch_add(1, Ordering::Relaxed);
} else {
self.control_success_count.fetch_add(1, Ordering::Relaxed);
}
} else {
if use_v22 {
self.v22_failure_count.fetch_add(1, Ordering::Relaxed);
} else {
self.control_failure_count.fetch_add(1, Ordering::Relaxed);
}
}
{
let tripwires = self.sla_tripwires.read().unwrap();
tripwires.heartbeat();
}
self.evaluate_stage_transition()?;
Ok(())
}
fn evaluate_stage_transition(&self) -> Result<()> {
let config = self.config.read().unwrap();
let current_stage = self.current_stage.read().unwrap();
let stage_start_time = *self.stage_start_time.read().unwrap();
let observation_hours = stage_start_time.elapsed().as_secs() as f64 / 3600.0;
if observation_hours < config.promotion_criteria.min_observation_hours as f64 {
return Ok(()); }
let metrics = self.compute_current_stage_metrics()?;
if self.should_auto_revert(&metrics)? {
self.trigger_auto_revert("SLA gate violations detected".to_string(), &metrics)?;
return Ok(());
}
if self.should_promote_stage(&metrics, &config.promotion_criteria)? {
if let Some(next_stage) = current_stage.next_stage() {
self.promote_to_stage(next_stage, "Promotion criteria met".to_string(), &metrics)?;
}
}
Ok(())
}
fn compute_current_stage_metrics(&self) -> Result<StageMetrics> {
let v22_success = self.v22_success_count.load(Ordering::Relaxed);
let v22_failure = self.v22_failure_count.load(Ordering::Relaxed);
let control_success = self.control_success_count.load(Ordering::Relaxed);
let control_failure = self.control_failure_count.load(Ordering::Relaxed);
let v22_total = v22_success + v22_failure;
let control_total = control_success + control_failure;
let v22_success_rate = if v22_total > 0 {
v22_success as f64 / v22_total as f64
} else {
1.0
};
let control_success_rate = if control_total > 0 {
control_success as f64 / control_total as f64
} else {
1.0
};
let stage_start_time = *self.stage_start_time.read().unwrap();
let observation_hours = stage_start_time.elapsed().as_secs() as f64 / 3600.0;
Ok(StageMetrics {
aece_value: 0.012, p99_latency_us: 850.0, success_rate: v22_success_rate,
confidence_shift: (v22_success_rate - control_success_rate).abs(),
health_status: HealthStatus::Green, observation_hours,
sample_count: v22_total,
})
}
fn should_auto_revert(&self, metrics: &StageMetrics) -> Result<bool> {
let config = self.config.read().unwrap();
if !config.auto_revert_config.enabled {
return Ok(false);
}
if metrics.p99_latency_us > config.sla_gates.max_p99_latency_increase_us {
warn!("P99 latency violation: {:.0}μs > {:.0}μs",
metrics.p99_latency_us, config.sla_gates.max_p99_latency_increase_us);
return Ok(true);
}
if metrics.aece_value > config.sla_gates.max_aece_tau_threshold {
warn!("AECE-τ threshold violation: {:.4} > {:.4}",
metrics.aece_value, config.sla_gates.max_aece_tau_threshold);
return Ok(true);
}
if metrics.confidence_shift > config.sla_gates.max_confidence_shift {
warn!("Confidence shift violation: {:.4} > {:.4}",
metrics.confidence_shift, config.sla_gates.max_confidence_shift);
return Ok(true);
}
{
let tripwires = self.sla_tripwires.read().unwrap();
if tripwires.should_block_calibration() {
warn!("SLA tripwires indicate calibration should be blocked");
return Ok(true);
}
}
Ok(false)
}
fn should_promote_stage(&self, metrics: &StageMetrics, criteria: &PromotionCriteria) -> Result<bool> {
if metrics.observation_hours < criteria.min_observation_hours as f64 {
return Ok(false);
}
if metrics.health_status != criteria.required_health_status {
return Ok(false);
}
if metrics.aece_value > criteria.max_aece_degradation {
return Ok(false);
}
if metrics.success_rate < criteria.min_success_rate {
return Ok(false);
}
Ok(true)
}
fn trigger_auto_revert(&self, reason: String, metrics: &StageMetrics) -> Result<()> {
let config = self.config.read().unwrap();
let daily_reverts = self.daily_revert_count.load(Ordering::Relaxed);
if daily_reverts >= config.auto_revert_config.max_reverts_per_day as u64 {
warn!("Daily auto-revert limit reached: {}/{}",
daily_reverts, config.auto_revert_config.max_reverts_per_day);
return Ok(());
}
self.circuit_breaker_open.store(true, Ordering::Relaxed);
{
let mut last_revert = self.last_revert_time.write().unwrap();
*last_revert = Some(Instant::now());
}
self.daily_revert_count.fetch_add(1, Ordering::Relaxed);
let current_stage = self.current_stage.read().unwrap().clone();
let transition = StageTransition {
timestamp: Utc::now(),
from_stage: format!("{:?}", current_stage),
to_stage: "Disabled".to_string(),
trigger_reason: format!("AUTO-REVERT: {}", reason),
metrics_snapshot: metrics.clone(),
config_fingerprint: config.config_fingerprint.clone(),
};
{
let mut stage = self.current_stage.write().unwrap();
*stage = RolloutStage::Disabled;
}
{
let mut history = self.transition_history.write().unwrap();
history.push(transition.clone());
let history_len = history.len();
if history_len > 100 {
history.drain(0..history_len - 100);
}
}
error!("🚨 AUTO-REVERT TRIGGERED: {}", reason);
error!("Transition: {} -> {}", transition.from_stage, transition.to_stage);
error!("Metrics: AECE={:.4}, P99={:.0}μs, Success={:.2}%, Shift={:.4}",
metrics.aece_value, metrics.p99_latency_us, metrics.success_rate * 100.0, metrics.confidence_shift);
Ok(())
}
fn promote_to_stage(&self, next_stage: RolloutStage, reason: String, metrics: &StageMetrics) -> Result<()> {
let config = self.config.read().unwrap();
let current_stage = self.current_stage.read().unwrap().clone();
let transition = StageTransition {
timestamp: Utc::now(),
from_stage: format!("{:?}", current_stage),
to_stage: format!("{:?}", next_stage),
trigger_reason: reason.clone(),
metrics_snapshot: metrics.clone(),
config_fingerprint: config.config_fingerprint.clone(),
};
{
let mut stage = self.current_stage.write().unwrap();
*stage = next_stage.clone();
}
{
let mut start_time = self.stage_start_time.write().unwrap();
*start_time = Instant::now();
}
{
let mut history = self.transition_history.write().unwrap();
history.push(transition.clone());
let history_len = history.len();
if history_len > 100 {
history.drain(0..history_len - 100);
}
}
info!("🎯 STAGE PROMOTION: {}", reason);
info!("Transition: {} -> {}", transition.from_stage, transition.to_stage);
info!("New rollout percentage: {}%", next_stage.get_percentage());
Ok(())
}
fn compute_repository_bucket(&self, repository_id: &str, strategy: &BucketStrategy) -> Result<u64> {
match &strategy.method {
BucketMethod::RepositoryHash { salt } => {
let mut hasher = DefaultHasher::new();
repository_id.hash(&mut hasher);
salt.hash(&mut hasher);
strategy.bucket_salt.hash(&mut hasher);
Ok(hasher.finish())
}
BucketMethod::Random => {
let mut hasher = DefaultHasher::new();
SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos().hash(&mut hasher);
Ok(hasher.finish())
}
BucketMethod::Manual { assignments } => {
if let Some(&enabled) = assignments.get(repository_id) {
Ok(if enabled { 0 } else { u64::MAX })
} else {
Ok(u64::MAX) }
}
}
}
fn record_decision(&self, decision: &FeatureFlagDecision) -> Result<()> {
let mut recent_decisions = self.recent_decisions.write().unwrap();
recent_decisions.push(decision.clone());
let decisions_len = recent_decisions.len();
if decisions_len > 1000 {
recent_decisions.drain(0..decisions_len - 1000);
}
Ok(())
}
pub fn get_status(&self) -> Result<serde_json::Value> {
let config = self.config.read().unwrap();
let current_stage = self.current_stage.read().unwrap();
let stage_start_time = *self.stage_start_time.read().unwrap();
let transition_history = self.transition_history.read().unwrap();
let v22_success = self.v22_success_count.load(Ordering::Relaxed);
let v22_failure = self.v22_failure_count.load(Ordering::Relaxed);
let control_success = self.control_success_count.load(Ordering::Relaxed);
let control_failure = self.control_failure_count.load(Ordering::Relaxed);
let v22_total = v22_success + v22_failure;
let control_total = control_success + control_failure;
let metrics = self.compute_current_stage_metrics()?;
Ok(serde_json::json!({
"enabled": config.enabled,
"current_stage": format!("{:?}", *current_stage),
"rollout_percentage": current_stage.get_percentage(),
"stage_duration_hours": stage_start_time.elapsed().as_secs() as f64 / 3600.0,
"circuit_breaker_open": self.circuit_breaker_open.load(Ordering::Relaxed),
"daily_reverts": self.daily_revert_count.load(Ordering::Relaxed),
"config_fingerprint": config.config_fingerprint,
"metrics": {
"v22_success": v22_success,
"v22_failure": v22_failure,
"v22_success_rate": if v22_total > 0 { v22_success as f64 / v22_total as f64 } else { 0.0 },
"control_success": control_success,
"control_failure": control_failure,
"control_success_rate": if control_total > 0 { control_success as f64 / control_total as f64 } else { 0.0 },
"current_stage_metrics": metrics
},
"transition_history_count": transition_history.len(),
"sla_gates": config.sla_gates,
"auto_revert_config": config.auto_revert_config,
}))
}
pub fn force_stage_transition(&self, target_stage: RolloutStage, reason: String) -> Result<()> {
let metrics = self.compute_current_stage_metrics()?;
if target_stage == RolloutStage::Disabled {
self.trigger_auto_revert(reason, &metrics)
} else {
self.promote_to_stage(target_stage, reason, &metrics)
}
}
pub fn reset_circuit_breaker(&self) -> Result<()> {
self.circuit_breaker_open.store(false, Ordering::Relaxed);
let metrics = self.compute_current_stage_metrics()?;
self.promote_to_stage(
RolloutStage::Canary,
"Circuit breaker reset - gradual recovery".to_string(),
&metrics
)?;
info!("🔄 Circuit breaker reset, returning to canary stage");
Ok(())
}
fn stage_from_percentage(percentage: u8) -> RolloutStage {
match percentage {
0 => RolloutStage::Disabled,
1..=5 => RolloutStage::Canary,
6..=25 => RolloutStage::Limited,
26..=50 => RolloutStage::Major,
51..=100 => RolloutStage::Full,
101..=u8::MAX => RolloutStage::Full, }
}
}
impl Default for CalibV22Config {
fn default() -> Self {
Self {
enabled: false, rollout_percentage: 0,
rollout_stage: "Disabled".to_string(),
bucket_strategy: BucketStrategy {
method: BucketMethod::RepositoryHash {
salt: "calib_v22_2025".to_string()
},
bucket_salt: "production_rollout".to_string(),
sticky_sessions: true,
override_buckets: HashMap::new(),
},
sla_gates: SlaGateConfig {
max_p99_latency_increase_us: 1000.0, max_aece_tau_threshold: 0.01,
max_confidence_shift: 0.02,
require_zero_sla_recall_change: true,
evaluation_window_minutes: 15,
consecutive_breach_threshold: 2,
},
auto_revert_config: AutoRevertConfig {
enabled: true,
breach_window_threshold: 2,
breach_window_duration_minutes: 15,
revert_cooldown_minutes: 60,
max_reverts_per_day: 5,
},
config_fingerprint: "default".to_string(),
rollout_start_time: Utc::now(),
promotion_criteria: PromotionCriteria {
min_observation_hours: 4,
required_health_status: HealthStatus::Green,
max_aece_degradation: 0.005,
require_p99_compliance: true,
min_success_rate: 0.99,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calibration::fast_bootstrap::FastBootstrapConfig;
#[tokio::test]
async fn test_feature_flag_creation() {
let config = CalibV22Config::default();
let sla_config = SlaConfig::default();
let drift_thresholds = DriftThresholds::default();
let binning_config = SharedBinningConfig::default();
let feature_flag = CalibV22FeatureFlag::new(
config, sla_config, drift_thresholds, binning_config
);
assert!(feature_flag.is_ok());
}
#[tokio::test]
async fn test_repository_bucketing() {
let config = CalibV22Config::default();
let sla_config = SlaConfig::default();
let drift_thresholds = DriftThresholds::default();
let binning_config = SharedBinningConfig::default();
let feature_flag = CalibV22FeatureFlag::new(
config, sla_config, drift_thresholds, binning_config
).unwrap();
let decision1 = feature_flag.should_use_calib_v22("repo123").unwrap();
let decision2 = feature_flag.should_use_calib_v22("repo123").unwrap();
assert_eq!(decision1.bucket_hash, decision2.bucket_hash);
assert_eq!(decision1.use_calib_v22, decision2.use_calib_v22);
}
#[tokio::test]
async fn test_status_reporting() {
let config = CalibV22Config::default();
let sla_config = SlaConfig::default();
let drift_thresholds = DriftThresholds::default();
let binning_config = SharedBinningConfig::default();
let feature_flag = CalibV22FeatureFlag::new(
config, sla_config, drift_thresholds, binning_config
).unwrap();
let status = feature_flag.get_status().unwrap();
assert!(status["enabled"].is_boolean());
assert!(status["current_stage"].is_string());
assert!(status["metrics"].is_object());
}
}