Files
el/ui/vessels/el-publish/src/rollout.rs
T

132 lines
4.0 KiB
Rust

//! Rollout monitor — checks crash rate, advances or halts rollout.
use crate::{config::RolloutConfig, PublishError, PublishResult};
/// Current rollout state.
#[derive(Debug, Clone)]
pub struct RolloutState {
pub track: String,
pub current_percent: u8,
pub crash_rate: f64,
pub is_halted: bool,
pub hours_elapsed: u64,
}
impl RolloutState {
pub fn new(track: impl Into<String>, initial_percent: u8) -> Self {
Self {
track: track.into(),
current_percent: initial_percent,
crash_rate: 0.0,
is_halted: false,
hours_elapsed: 0,
}
}
}
/// Monitors a staged rollout and decides whether to advance or halt.
pub struct RolloutMonitor {
pub config: RolloutConfig,
}
impl RolloutMonitor {
pub fn new(config: RolloutConfig) -> Self {
Self { config }
}
/// Evaluate whether to advance, halt, or maintain the current rollout.
pub fn evaluate(&self, state: &RolloutState) -> RolloutDecision {
if state.is_halted {
return RolloutDecision::Halted {
reason: "rollout was previously halted".into(),
};
}
// Check crash rate
if state.crash_rate > self.config.max_crash_rate {
return RolloutDecision::Halt {
reason: format!(
"crash rate {:.2}% exceeds threshold {:.2}%",
state.crash_rate * 100.0,
self.config.max_crash_rate * 100.0
),
};
}
// Check if we should advance
if self.config.auto_advance
&& state.hours_elapsed >= self.config.advance_after_hours
&& state.current_percent < 100
{
let next_percent = advance_percent(state.current_percent);
return RolloutDecision::Advance { to_percent: next_percent };
}
RolloutDecision::Maintain
}
/// Apply a rollout decision — returns the new rollout state.
pub fn apply(
&self,
mut state: RolloutState,
decision: &RolloutDecision,
) -> PublishResult<RolloutState> {
match decision {
RolloutDecision::Advance { to_percent } => {
state.current_percent = *to_percent;
state.hours_elapsed = 0;
}
RolloutDecision::Halt { reason } => {
state.is_halted = true;
let _ = reason;
}
RolloutDecision::Halted { .. } => {
return Err(PublishError::Config(
"cannot apply decision to halted rollout".into(),
));
}
RolloutDecision::Maintain => {}
}
Ok(state)
}
/// Run a complete rollout cycle: fetch metrics, evaluate, apply.
///
/// In production: fetch crash rate from Firebase Crashlytics or App Store
/// Connect Analytics API.
pub fn tick(&self, mut state: RolloutState) -> PublishResult<(RolloutState, RolloutDecision)> {
// TODO: fetch real crash rate from analytics API
// let crash_rate = analytics_client.crash_rate(&state.track, &config.build).await?;
// state.crash_rate = crash_rate;
state.hours_elapsed += 1;
let decision = self.evaluate(&state);
let new_state = self.apply(state, &decision)?;
Ok((new_state, decision))
}
}
/// The outcome of a rollout evaluation.
#[derive(Debug, Clone)]
pub enum RolloutDecision {
/// Advance to a higher rollout percentage.
Advance { to_percent: u8 },
/// Halt the rollout due to high crash rate.
Halt { reason: String },
/// Rollout was already halted.
Halted { reason: String },
/// No change needed.
Maintain,
}
/// Determine the next rollout percentage.
/// Uses the standard staged-rollout progression: 10 → 25 → 50 → 100.
fn advance_percent(current: u8) -> u8 {
match current {
0..=9 => 10,
10..=24 => 25,
25..=49 => 50,
_ => 100,
}
}