feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
//! 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user