use std::ops::Range;
use async_trait::async_trait;
use crate::SimulationResult;
use super::context::SimContext;
#[async_trait]
pub trait Process: Send + Sync + 'static {
fn name(&self) -> &str;
async fn run(&mut self, ctx: &SimContext) -> SimulationResult<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RebootKind {
Graceful,
Crash,
CrashAndWipe,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AttritionScope {
#[default]
PerProcess,
PerMachine,
PerZone,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Attrition {
pub max_dead: usize,
pub prob_graceful: f64,
pub prob_crash: f64,
pub prob_wipe: f64,
pub recovery_delay_ms: Option<Range<usize>>,
pub grace_period_ms: Option<Range<usize>>,
pub scope: AttritionScope,
}
impl Attrition {
pub(crate) fn choose_kind(&self, rand_val: f64) -> RebootKind {
let total = self.prob_graceful + self.prob_crash + self.prob_wipe;
if total <= 0.0 {
return RebootKind::Crash;
}
let normalized = rand_val * total;
if normalized < self.prob_graceful {
RebootKind::Graceful
} else if normalized < self.prob_graceful + self.prob_crash {
RebootKind::Crash
} else {
RebootKind::CrashAndWipe
}
}
#[must_use]
pub fn swarm_for_seed(&self) -> Attrition {
let mut regime = self.clone();
if !crate::sim::config_random_bool(0.5) {
regime.max_dead = 0;
}
if !crate::sim::config_random_bool(0.5) {
regime.prob_graceful = 0.0;
}
if !crate::sim::config_random_bool(0.5) {
regime.prob_crash = 0.0;
}
if !crate::sim::config_random_bool(0.5) {
regime.prob_wipe = 0.0;
}
regime
}
}
#[cfg(test)]
mod swarm_tests {
use super::{Attrition, AttritionScope};
use crate::sim::rng::set_config_seed;
fn base() -> Attrition {
Attrition {
max_dead: 2,
prob_graceful: 0.3,
prob_crash: 0.5,
prob_wipe: 0.2,
recovery_delay_ms: None,
grace_period_ms: None,
scope: AttritionScope::PerProcess,
}
}
fn swarm_for(seed: u64) -> Attrition {
set_config_seed(seed);
base().swarm_for_seed()
}
#[test]
fn swarm_regime_is_deterministic_per_seed() {
for seed in [0_u64, 1, 42, 12_345] {
assert_eq!(
swarm_for(seed),
swarm_for(seed),
"swarm regime must be reproducible for seed {seed}"
);
}
}
#[test]
fn swarm_reaches_never_reboot() {
let saw_never_reboot = (0..1000_u64).any(|s| swarm_for(s).max_dead == 0);
assert!(
saw_never_reboot,
"no seed in 0..1000 produced the never-reboot regime (max_dead == 0)"
);
}
#[test]
fn swarm_reaches_single_mode() {
let saw_single_mode = (0..1000_u64).any(|s| {
let r = swarm_for(s);
let on = [r.prob_graceful, r.prob_crash, r.prob_wipe]
.iter()
.filter(|&&w| w > 0.0)
.count();
on == 1
});
assert!(
saw_single_mode,
"no seed in 0..1000 produced a single-mode regime (one kind enabled)"
);
}
}