use std::collections::HashSet;
pub struct ChaosController {
enabled: bool,
seed: Option<u64>,
scope: Option<HashSet<String>>,
production_detected: bool,
counter: u64,
}
impl ChaosController {
pub fn new() -> Self {
let production = ChaosController::detect_production();
ChaosController {
enabled: !production && ChaosController::env_chaos_enabled(),
seed: ChaosController::env_chaos_seed(),
scope: ChaosController::env_chaos_scope(),
production_detected: production,
counter: 0,
}
}
pub fn should_inject_chaos(&mut self, node_id: &str) -> bool {
if !self.enabled || self.production_detected {
return false;
}
if let Some(ref scope) = self.scope {
if !scope.contains(node_id) && !scope.contains("*") {
return false;
}
}
self.counter += 1;
if let Some(seed) = self.seed {
let hash = simple_hash(seed, self.counter, node_id);
hash % 2 == 0
} else {
self.counter % 2 == 0
}
}
fn detect_production() -> bool {
let env = std::env::var("ETDL_ENV")
.or_else(|_| std::env::var("DEPLOYMENT_ENVIRONMENT"))
.or_else(|_| std::env::var("ENV"))
.unwrap_or_default();
matches!(
env.to_lowercase().as_str(),
"production" | "prod" | "prd" | "live"
)
}
fn env_chaos_enabled() -> bool {
std::env::var("ETDL_CHAOS")
.map(|v| matches!(v.to_lowercase().as_str(), "true" | "1" | "yes" | "on"))
.unwrap_or(false)
}
fn env_chaos_seed() -> Option<u64> {
std::env::var("ETDL_CHAOS_SEED")
.ok()
.and_then(|v| v.parse().ok())
}
fn env_chaos_scope() -> Option<HashSet<String>> {
std::env::var("ETDL_CHAOS_SCOPE")
.ok()
.map(|v| v.split(',').map(|s| s.trim().to_string()).collect())
}
}
fn simple_hash(seed: u64, counter: u64, node_id: &str) -> u64 {
let mut hash = seed.wrapping_mul(0x9E3779B9);
hash = hash.wrapping_add(counter);
for byte in node_id.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(byte as u64);
}
hash
}
impl Default for ChaosController {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chaos_disabled_in_production() {
std::env::set_var("ETDL_ENV", "production");
std::env::set_var("ETDL_CHAOS", "true");
let mut controller = ChaosController::new();
assert!(!controller.should_inject_chaos("test_node"));
std::env::remove_var("ETDL_ENV");
std::env::remove_var("ETDL_CHAOS");
}
#[test]
fn test_chaos_scope_filtering() {
std::env::set_var("ETDL_CHAOS", "true");
std::env::set_var("ETDL_CHAOS_SCOPE", "node_a,node_b");
let mut controller = ChaosController::new();
assert!(controller.scope.as_ref().unwrap().contains("node_a"));
assert!(!controller.scope.as_ref().unwrap().contains("node_c"));
std::env::remove_var("ETDL_CHAOS");
std::env::remove_var("ETDL_CHAOS_SCOPE");
}
#[test]
fn test_chaos_deterministic_with_seed() {
std::env::set_var("ETDL_CHAOS", "true");
std::env::set_var("ETDL_CHAOS_SEED", "42");
let mut c1 = ChaosController::new();
let mut c2 = ChaosController::new();
let r1 = c1.should_inject_chaos("test");
let r2 = c2.should_inject_chaos("test");
assert_eq!(r1, r2);
std::env::remove_var("ETDL_CHAOS");
std::env::remove_var("ETDL_CHAOS_SEED");
}
}