use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
#[derive(Debug, Clone, Copy, Default)]
pub struct ChaosConfig {
pub enabled: bool,
pub drop_pct: u8,
pub reorder_pct: u8,
pub stall: Duration,
pub seed: u64,
}
impl ChaosConfig {
fn from_env_str(s: &str) -> Self {
let mut cfg = Self {
enabled: !s.trim().is_empty(),
..Self::default()
};
for tok in s.split(',') {
let tok = tok.trim();
if tok.is_empty() {
continue;
}
let (k, v) = match tok.split_once(':') {
Some(p) => p,
None => continue,
};
let v = v.trim().trim_end_matches(['%', 's', 'm']);
match k.trim() {
"drop" => cfg.drop_pct = v.parse().unwrap_or(0).min(100),
"reorder" => cfg.reorder_pct = v.parse().unwrap_or(0).min(100),
"stall" => {
let raw = tok.split_once(':').unwrap().1;
cfg.stall = parse_duration(raw);
}
"seed" => cfg.seed = v.parse().unwrap_or(0),
_ => {}
}
}
cfg
}
}
fn parse_duration(s: &str) -> Duration {
let s = s.trim();
if let Some(num) = s.strip_suffix("ms") {
return Duration::from_millis(num.parse().unwrap_or(0));
}
if let Some(num) = s.strip_suffix('s') {
return Duration::from_secs(num.parse().unwrap_or(0));
}
Duration::from_millis(s.parse().unwrap_or(0))
}
static CONFIG: OnceLock<ChaosConfig> = OnceLock::new();
static COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn config() -> &'static ChaosConfig {
CONFIG.get_or_init(
|| match epics_base_rs::runtime::env::get("EPICS_CA_RS_CHAOS") {
Some(s) => ChaosConfig::from_env_str(&s),
None => ChaosConfig::default(),
},
)
}
#[inline]
pub fn enabled() -> bool {
config().enabled
}
fn rand_u32() -> u32 {
let cfg = config();
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let s = cfg.seed ^ n;
let mut x = s.wrapping_add(0x9E3779B97F4A7C15);
x = (x ^ (x >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
x = (x ^ (x >> 27)).wrapping_mul(0x94D049BB133111EB);
x ^= x >> 31;
x as u32
}
fn roll(pct: u8) -> bool {
if pct == 0 {
return false;
}
if pct >= 100 {
return true;
}
let r = rand_u32() % 100;
r < pct as u32
}
pub async fn maybe_stall() {
let cfg = config();
if cfg.enabled && cfg.stall > Duration::ZERO {
tokio::time::sleep(cfg.stall).await;
}
}
pub fn should_drop_read() -> bool {
let cfg = config();
cfg.enabled && roll(cfg.drop_pct)
}
pub fn reorder_delay() -> Duration {
let cfg = config();
if !cfg.enabled || !roll(cfg.reorder_pct) {
return Duration::ZERO;
}
Duration::from_micros(((rand_u32() % 5_000) + 100) as u64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_disabled_when_unset() {
let c = ChaosConfig::from_env_str("");
assert!(!c.enabled);
}
#[test]
fn parse_drop_and_stall() {
let c = ChaosConfig::from_env_str("drop:5%,stall:50ms");
assert!(c.enabled);
assert_eq!(c.drop_pct, 5);
assert_eq!(c.stall, Duration::from_millis(50));
}
#[test]
fn parse_seconds_stall() {
let c = ChaosConfig::from_env_str("stall:2s");
assert_eq!(c.stall, Duration::from_secs(2));
}
#[test]
fn parse_seed_is_deterministic() {
let c = ChaosConfig::from_env_str("drop:50%,seed:42");
assert_eq!(c.seed, 42);
}
#[test]
fn unknown_keys_are_ignored() {
let c = ChaosConfig::from_env_str("totally_made_up:1,drop:1%");
assert_eq!(c.drop_pct, 1);
}
#[test]
fn roll_extremes_are_honoured() {
for _ in 0..1000 {
assert!(!roll(0));
assert!(roll(100));
}
}
}