use std::ops::Range;
#[derive(Debug, Clone)]
pub struct FaultConfig {
pub base_latency_ms: u64,
pub latency_jitter_ms: u64,
pub message_loss_rate: f64,
pub reorder_window_ms: u64,
pub duplicate_rate: f64,
pub partition_probability: f64,
pub partition_duration_ms: Range<u64>,
pub crash_probability: f64,
pub restart_delay_ms: Range<u64>,
pub slow_node_factor: f64,
pub write_during_sync_rate: f64,
}
impl Default for FaultConfig {
fn default() -> Self {
Self {
base_latency_ms: 10,
latency_jitter_ms: 5,
message_loss_rate: 0.0,
reorder_window_ms: 0,
duplicate_rate: 0.0,
partition_probability: 0.0,
partition_duration_ms: 0..0,
crash_probability: 0.0,
restart_delay_ms: 0..0,
slow_node_factor: 1.0,
write_during_sync_rate: 0.0,
}
}
}
impl FaultConfig {
pub fn none() -> Self {
Self {
base_latency_ms: 0,
latency_jitter_ms: 0,
..Default::default()
}
}
pub fn light_chaos() -> Self {
Self {
base_latency_ms: 10,
latency_jitter_ms: 5,
message_loss_rate: 0.01,
reorder_window_ms: 20,
duplicate_rate: 0.01,
..Default::default()
}
}
pub fn heavy_chaos() -> Self {
Self {
base_latency_ms: 50,
latency_jitter_ms: 25,
message_loss_rate: 0.1,
reorder_window_ms: 100,
duplicate_rate: 0.05,
partition_probability: 0.01,
partition_duration_ms: 100..500,
crash_probability: 0.001,
restart_delay_ms: 100..1000,
..Default::default()
}
}
pub fn partition_heavy() -> Self {
Self {
base_latency_ms: 10,
latency_jitter_ms: 5,
partition_probability: 0.1,
partition_duration_ms: 200..1000,
..Default::default()
}
}
pub fn with_latency(mut self, base_ms: u64, jitter_ms: u64) -> Self {
self.base_latency_ms = base_ms;
self.latency_jitter_ms = jitter_ms;
self
}
pub fn with_loss(mut self, rate: f64) -> Self {
self.message_loss_rate = rate.clamp(0.0, 1.0);
self
}
pub fn with_duplicates(mut self, rate: f64) -> Self {
self.duplicate_rate = rate.clamp(0.0, 1.0);
self
}
pub fn with_reorder(mut self, window_ms: u64) -> Self {
self.reorder_window_ms = window_ms;
self
}
pub fn with_partitions(mut self, probability: f64, duration_ms: Range<u64>) -> Self {
self.partition_probability = probability.clamp(0.0, 1.0);
self.partition_duration_ms = duration_ms;
self
}
pub fn with_crashes(mut self, probability: f64, restart_delay_ms: Range<u64>) -> Self {
self.crash_probability = probability.clamp(0.0, 1.0);
self.restart_delay_ms = restart_delay_ms;
self
}
pub fn validate(&self) -> Result<(), String> {
if !(0.0..=1.0).contains(&self.message_loss_rate) {
return Err("message_loss_rate must be in [0.0, 1.0]".to_string());
}
if !(0.0..=1.0).contains(&self.duplicate_rate) {
return Err("duplicate_rate must be in [0.0, 1.0]".to_string());
}
if !(0.0..=1.0).contains(&self.partition_probability) {
return Err("partition_probability must be in [0.0, 1.0]".to_string());
}
if !(0.0..=1.0).contains(&self.crash_probability) {
return Err("crash_probability must be in [0.0, 1.0]".to_string());
}
if self.slow_node_factor < 0.0 {
return Err("slow_node_factor must be non-negative".to_string());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = FaultConfig::default();
assert!(config.validate().is_ok());
assert_eq!(config.message_loss_rate, 0.0);
}
#[test]
fn test_none_config() {
let config = FaultConfig::none();
assert_eq!(config.base_latency_ms, 0);
assert_eq!(config.message_loss_rate, 0.0);
}
#[test]
fn test_chaos_configs() {
let light = FaultConfig::light_chaos();
assert!(light.validate().is_ok());
assert!(light.message_loss_rate > 0.0);
let heavy = FaultConfig::heavy_chaos();
assert!(heavy.validate().is_ok());
assert!(heavy.message_loss_rate > light.message_loss_rate);
}
#[test]
fn test_builder_pattern() {
let config = FaultConfig::none()
.with_latency(50, 10)
.with_loss(0.05)
.with_duplicates(0.02)
.with_reorder(100);
assert!(config.validate().is_ok());
assert_eq!(config.base_latency_ms, 50);
assert_eq!(config.latency_jitter_ms, 10);
assert_eq!(config.message_loss_rate, 0.05);
assert_eq!(config.duplicate_rate, 0.02);
assert_eq!(config.reorder_window_ms, 100);
}
#[test]
fn test_validation_clamps() {
let config = FaultConfig::none().with_loss(2.0);
assert_eq!(config.message_loss_rate, 1.0);
let config = FaultConfig::none().with_loss(-0.5);
assert_eq!(config.message_loss_rate, 0.0);
}
}