132 lines
4.0 KiB
Rust
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,
|
|
}
|
|
}
|