use crate::sim::rng::{
config_random_bool, sim_random_f64, sim_random_range, sim_random_range_or_default,
};
use std::ops::Range;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PartitionStrategy {
#[default]
Random,
UniformSize,
IsolateSingle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConnectFailureMode {
#[default]
Disabled,
AlwaysFail,
Probabilistic,
}
impl ConnectFailureMode {
#[must_use]
pub fn random_for_seed() -> Self {
match sim_random_range(0..3) {
0 => Self::Disabled,
1 => Self::AlwaysFail,
_ => Self::Probabilistic,
}
}
}
#[derive(Debug, Clone)]
pub struct ChaosConfiguration {
pub clog_probability: f64,
pub clog_duration: Range<Duration>,
pub partition_probability: f64,
pub partition_duration: Range<Duration>,
pub bit_flip_probability: f64,
pub bit_flip_min_bits: u32,
pub bit_flip_max_bits: u32,
pub bit_flip_cooldown: Duration,
pub partial_write_max_bytes: usize,
pub partial_read_max_bytes: usize,
pub random_close_probability: f64,
pub random_close_cooldown: Duration,
pub random_close_explicit_ratio: f64,
pub clock_drift_enabled: bool,
pub clock_drift_max: Duration,
pub buggified_delay_enabled: bool,
pub buggified_delay_max: Duration,
pub buggified_delay_probability: f64,
pub connect_failure_mode: ConnectFailureMode,
pub connect_failure_probability: f64,
pub max_pair_latency: Range<Duration>,
pub partition_strategy: PartitionStrategy,
}
impl Default for ChaosConfiguration {
fn default() -> Self {
Self {
clog_probability: 0.0,
clog_duration: Duration::from_millis(100)..Duration::from_millis(300),
partition_probability: 0.0,
partition_duration: Duration::from_millis(200)..Duration::from_secs(2),
bit_flip_probability: 0.0001, bit_flip_min_bits: 1,
bit_flip_max_bits: 32,
bit_flip_cooldown: Duration::ZERO, partial_write_max_bytes: 1000, partial_read_max_bytes: 1000, random_close_probability: 0.00001, random_close_cooldown: Duration::from_secs(5), random_close_explicit_ratio: 0.3, clock_drift_enabled: true, clock_drift_max: Duration::from_millis(100), buggified_delay_enabled: true, buggified_delay_max: Duration::from_millis(100), buggified_delay_probability: 0.25, connect_failure_mode: ConnectFailureMode::Probabilistic, connect_failure_probability: 0.5, max_pair_latency: Duration::ZERO..Duration::ZERO, partition_strategy: PartitionStrategy::default(),
}
}
}
impl ChaosConfiguration {
#[must_use]
pub fn disabled() -> Self {
Self {
clog_probability: 0.0,
clog_duration: Duration::ZERO..Duration::ZERO,
partition_probability: 0.0,
partition_duration: Duration::ZERO..Duration::ZERO,
bit_flip_probability: 0.0,
bit_flip_min_bits: 1,
bit_flip_max_bits: 32,
bit_flip_cooldown: Duration::ZERO,
partial_write_max_bytes: 1000,
partial_read_max_bytes: 1000,
random_close_probability: 0.0,
random_close_cooldown: Duration::ZERO,
random_close_explicit_ratio: 0.3,
clock_drift_enabled: false,
clock_drift_max: Duration::from_millis(100),
buggified_delay_enabled: false,
buggified_delay_max: Duration::from_millis(100),
buggified_delay_probability: 0.25,
connect_failure_mode: ConnectFailureMode::Disabled,
connect_failure_probability: 0.5,
max_pair_latency: Duration::ZERO..Duration::ZERO,
partition_strategy: PartitionStrategy::Random, }
}
#[must_use]
pub fn random_for_seed() -> Self {
Self {
clog_probability: f64::from(sim_random_range(0..20)) / 100.0, clog_duration: Duration::from_micros(sim_random_range(50_000..300_000))
..Duration::from_micros(sim_random_range(100_000..500_000)),
partition_probability: f64::from(sim_random_range(0..15)) / 100.0, partition_duration: Duration::from_millis(sim_random_range(100..1000))
..Duration::from_millis(sim_random_range(500..3000)),
bit_flip_probability: f64::from(sim_random_range(1..20)) / 100_000.0,
bit_flip_min_bits: 1,
bit_flip_max_bits: 32,
bit_flip_cooldown: Duration::from_millis(sim_random_range(0..100)),
partial_write_max_bytes: sim_random_range(100..2000), random_close_probability: f64::from(sim_random_range(1..100)) / 1_000_000.0,
random_close_cooldown: Duration::from_millis(sim_random_range(1000..10_000)),
random_close_explicit_ratio: f64::from(sim_random_range(20..40)) / 100.0, clock_drift_enabled: true,
clock_drift_max: Duration::from_millis(sim_random_range(50..150)), buggified_delay_enabled: true,
buggified_delay_max: Duration::from_millis(sim_random_range(50..150)), buggified_delay_probability: f64::from(sim_random_range(20..30)) / 100.0, connect_failure_mode: ConnectFailureMode::random_for_seed(),
connect_failure_probability: f64::from(sim_random_range(40..60)) / 100.0, partition_strategy: match sim_random_range(0..3) {
0 => PartitionStrategy::Random,
1 => PartitionStrategy::UniformSize,
_ => PartitionStrategy::IsolateSingle,
},
max_pair_latency: Duration::ZERO..Duration::from_millis(sim_random_range(0..100)),
partial_read_max_bytes: sim_random_range(100..2000),
}
}
#[must_use]
pub fn swarm_for_seed() -> Self {
let mut chaos = Self::random_for_seed();
chaos.apply_swarm_mask();
chaos
}
fn apply_swarm_mask(&mut self) {
if !config_random_bool(0.5) {
self.clog_probability = 0.0;
}
if !config_random_bool(0.5) {
self.partition_probability = 0.0;
}
if !config_random_bool(0.5) {
self.bit_flip_probability = 0.0;
}
if !config_random_bool(0.5) {
self.random_close_probability = 0.0;
}
if !config_random_bool(0.5) {
self.connect_failure_mode = ConnectFailureMode::Disabled;
self.connect_failure_probability = 0.0;
}
self.clock_drift_enabled = config_random_bool(0.5);
self.buggified_delay_enabled = config_random_bool(0.5);
if !config_random_bool(0.5) {
self.max_pair_latency = Duration::ZERO..Duration::ZERO;
}
}
pub fn apply_buggify_knobs(&mut self) {
self.clog_probability = crate::buggify_knob!(self.clog_probability, 0.5..1.0);
self.partition_probability = crate::buggify_knob!(self.partition_probability, 0.3..0.8);
self.random_close_probability =
crate::buggify_knob!(self.random_close_probability, 0.01..0.1);
}
}
#[derive(Debug, Clone)]
pub struct NetworkConfiguration {
pub bind_latency: LatencyDistribution,
pub accept_latency: LatencyDistribution,
pub connect_latency: LatencyDistribution,
pub read_latency: LatencyDistribution,
pub write_latency: LatencyDistribution,
pub chaos: ChaosConfiguration,
}
impl Default for NetworkConfiguration {
fn default() -> Self {
Self {
bind_latency: LatencyDistribution::Uniform {
start: Duration::from_micros(50),
end: Duration::from_micros(150),
},
accept_latency: LatencyDistribution::Uniform {
start: Duration::from_millis(1),
end: Duration::from_millis(6),
},
connect_latency: LatencyDistribution::Uniform {
start: Duration::from_millis(1),
end: Duration::from_millis(11),
},
read_latency: LatencyDistribution::Uniform {
start: Duration::from_micros(10),
end: Duration::from_micros(60),
},
write_latency: LatencyDistribution::Uniform {
start: Duration::from_micros(100),
end: Duration::from_micros(600),
},
chaos: ChaosConfiguration::default(),
}
}
}
#[must_use]
pub fn sample_duration(range: &Range<Duration>) -> Duration {
uniform_nanos(range.start, range.end)
}
fn uniform_nanos(start: Duration, end: Duration) -> Duration {
let start_nanos = u64::try_from(start.as_nanos()).unwrap_or(u64::MAX);
let end_nanos = u64::try_from(end.as_nanos()).unwrap_or(u64::MAX);
Duration::from_nanos(sim_random_range_or_default(start_nanos..end_nanos))
}
#[derive(Debug, Clone, PartialEq)]
pub enum LatencyDistribution {
Uniform {
start: Duration,
end: Duration,
},
Exponential {
min: Duration,
mean: Duration,
},
Bimodal {
fast_range: Range<Duration>,
slow_range: Range<Duration>,
slow_probability: f64,
},
}
impl Default for LatencyDistribution {
fn default() -> Self {
Self::Uniform {
start: Duration::ZERO,
end: Duration::ZERO,
}
}
}
impl LatencyDistribution {
#[must_use]
pub fn uniform_bounds(&self) -> Option<(Duration, Duration)> {
match self {
Self::Uniform { start, end } => Some((*start, *end)),
_ => None,
}
}
}
#[must_use]
pub fn sample_latency(distribution: &LatencyDistribution) -> Duration {
match distribution {
LatencyDistribution::Uniform { start, end } => uniform_nanos(*start, *end),
LatencyDistribution::Exponential { min, mean } => {
let u = sim_random_f64();
let factor = -(1.0 - u).ln();
let extra_secs = mean.as_secs_f64() * factor;
let extra = Duration::try_from_secs_f64(extra_secs).unwrap_or(Duration::MAX);
min.saturating_add(extra)
}
LatencyDistribution::Bimodal {
fast_range,
slow_range,
slow_probability,
} => {
if sim_random_f64() < *slow_probability {
uniform_nanos(slow_range.start, slow_range.end)
} else {
uniform_nanos(fast_range.start, fast_range.end)
}
}
}
}
pub(crate) fn random_latency_for_seed(uniform: Range<Duration>) -> LatencyDistribution {
match sim_random_range(0..3) {
0 => LatencyDistribution::Uniform {
start: uniform.start,
end: uniform.end,
},
1 => LatencyDistribution::Exponential {
min: uniform.start,
mean: uniform.end.saturating_sub(uniform.start),
},
_ => {
let slow_start = uniform.end;
let slow_end = uniform.end.saturating_mul(10);
LatencyDistribution::Bimodal {
fast_range: uniform,
slow_range: slow_start..slow_end,
slow_probability: f64::from(sim_random_range(1..10)) / 1000.0,
}
}
}
}
impl NetworkConfiguration {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn random_for_seed() -> Self {
Self {
bind_latency: random_latency_for_seed(
Duration::from_micros(sim_random_range(10..200))
..Duration::from_micros(sim_random_range(50..300)),
),
accept_latency: random_latency_for_seed(
Duration::from_micros(sim_random_range(1000..10_000))
..Duration::from_micros(sim_random_range(5000..15_000)),
),
connect_latency: random_latency_for_seed(
Duration::from_micros(sim_random_range(1000..50_000))
..Duration::from_micros(sim_random_range(10_000..100_000)),
),
read_latency: random_latency_for_seed(
Duration::from_micros(sim_random_range(5..100))
..Duration::from_micros(sim_random_range(50..200)),
),
write_latency: random_latency_for_seed(
Duration::from_micros(sim_random_range(50..1000))
..Duration::from_micros(sim_random_range(200..2000)),
),
chaos: ChaosConfiguration::random_for_seed(),
}
}
#[must_use]
pub fn swarm_for_seed() -> Self {
let mut config = Self::random_for_seed();
config.chaos.apply_swarm_mask();
config
}
#[must_use]
pub fn fast_local() -> Self {
let one_us = Duration::from_micros(1);
let ten_us = Duration::from_micros(10);
let uniform = |start, end| LatencyDistribution::Uniform { start, end };
Self {
bind_latency: uniform(one_us, one_us),
accept_latency: uniform(ten_us, ten_us),
connect_latency: uniform(ten_us, ten_us),
read_latency: uniform(one_us, one_us),
write_latency: uniform(one_us, one_us),
chaos: ChaosConfiguration::disabled(),
}
}
}
#[cfg(test)]
mod swarm_tests {
use super::{ChaosConfiguration, ConnectFailureMode, NetworkConfiguration};
use crate::sim::rng::{reset_sim_rng, set_config_seed, set_sim_seed};
fn enabled_families(chaos: &ChaosConfiguration) -> [bool; 7] {
[
chaos.clog_probability > 0.0,
chaos.partition_probability > 0.0,
chaos.bit_flip_probability > 0.0,
chaos.random_close_probability > 0.0,
chaos.connect_failure_mode != ConnectFailureMode::Disabled,
chaos.clock_drift_enabled,
chaos.buggified_delay_enabled,
]
}
fn swarm_for(seed: u64) -> NetworkConfiguration {
reset_sim_rng();
set_sim_seed(seed);
set_config_seed(seed);
NetworkConfiguration::swarm_for_seed()
}
#[test]
fn swarm_subset_is_deterministic_per_seed() {
for seed in [0_u64, 1, 42, 12_345] {
let first = enabled_families(&swarm_for(seed).chaos);
let second = enabled_families(&swarm_for(seed).chaos);
assert_eq!(
first, second,
"swarm subset must be reproducible for seed {seed}"
);
}
}
#[test]
fn swarm_reaches_all_off_and_mixed_subsets() {
let mut saw_all_off = false;
let mut saw_mixed = false;
for seed in 0..1000_u64 {
let families = enabled_families(&swarm_for(seed).chaos);
let on = families.iter().filter(|&&e| e).count();
if on == 0 {
saw_all_off = true;
}
if on > 0 && on < families.len() {
saw_mixed = true;
}
if saw_all_off && saw_mixed {
break;
}
}
assert!(
saw_all_off,
"no seed in 0..1000 produced the all-off subset"
);
assert!(saw_mixed, "no seed in 0..1000 produced a mixed subset");
}
#[test]
fn swarm_all_off_seed_has_zero_fault_probabilities() {
let seed = (0..1000_u64)
.find(|&s| enabled_families(&swarm_for(s).chaos).iter().all(|&e| !e))
.expect("expected an all-off seed within 0..1000");
let chaos = swarm_for(seed).chaos;
assert_zero(chaos.clog_probability);
assert_zero(chaos.partition_probability);
assert_zero(chaos.bit_flip_probability);
assert_zero(chaos.random_close_probability);
assert_eq!(chaos.connect_failure_mode, ConnectFailureMode::Disabled);
assert_zero(chaos.connect_failure_probability);
assert!(!chaos.clock_drift_enabled);
assert!(!chaos.buggified_delay_enabled);
}
fn assert_zero(value: f64) {
assert_eq!(
value.to_bits(),
0.0_f64.to_bits(),
"expected 0.0, got {value}"
);
}
}
#[cfg(test)]
mod latency_distribution_tests {
use super::{LatencyDistribution, NetworkConfiguration, sample_duration, sample_latency};
use crate::sim::rng::set_sim_seed;
use crate::storage::StorageConfiguration;
use std::time::Duration;
fn samples(seed: u64, dist: &LatencyDistribution, n: usize) -> Vec<Duration> {
set_sim_seed(seed);
(0..n).map(|_| sample_latency(dist)).collect()
}
fn p99(mut values: Vec<Duration>) -> Duration {
values.sort_unstable();
let idx = ((values.len() * 99) / 100).min(values.len() - 1);
values[idx]
}
#[test]
fn uniform_matches_sample_duration_byte_for_byte() {
let start = Duration::from_micros(100);
let end = Duration::from_micros(600);
let dist = LatencyDistribution::Uniform { start, end };
let range = start..end;
set_sim_seed(7);
let a1 = sample_latency(&dist);
let a2 = sample_duration(&range);
set_sim_seed(7);
let b1 = sample_duration(&range);
let b2 = sample_duration(&range);
assert_eq!((a1, a2), (b1, b2));
}
#[test]
fn each_variant_is_deterministic_per_seed() {
let variants = [
LatencyDistribution::Uniform {
start: Duration::from_micros(10),
end: Duration::from_micros(60),
},
LatencyDistribution::Exponential {
min: Duration::from_micros(10),
mean: Duration::from_micros(100),
},
LatencyDistribution::Bimodal {
fast_range: Duration::from_millis(1)..Duration::from_millis(2),
slow_range: Duration::from_millis(50)..Duration::from_millis(100),
slow_probability: 0.05,
},
];
for dist in &variants {
let first = samples(42, dist, 64);
let second = samples(42, dist, 64);
assert_eq!(first, second, "distribution not deterministic: {dist:?}");
}
}
#[test]
fn default_configs_are_all_uniform() {
let net = NetworkConfiguration::default();
for dist in [
&net.bind_latency,
&net.accept_latency,
&net.connect_latency,
&net.read_latency,
&net.write_latency,
] {
assert!(
dist.uniform_bounds().is_some(),
"network default not uniform: {dist:?}"
);
}
let storage = StorageConfiguration::default();
for dist in [
&storage.read_latency,
&storage.write_latency,
&storage.sync_latency,
] {
assert!(
dist.uniform_bounds().is_some(),
"storage default not uniform: {dist:?}"
);
}
}
#[test]
fn exponential_has_heavier_tail_than_uniform_at_equal_mean() {
let uniform = LatencyDistribution::Uniform {
start: Duration::ZERO,
end: Duration::from_millis(2),
};
let exponential = LatencyDistribution::Exponential {
min: Duration::ZERO,
mean: Duration::from_millis(1),
};
let uni_p99 = p99(samples(123, &uniform, 10_000));
let exp_p99 = p99(samples(123, &exponential, 10_000));
assert!(
exp_p99 > uni_p99,
"exponential p99 {exp_p99:?} should exceed uniform p99 {uni_p99:?}"
);
}
#[test]
fn bimodal_shows_fast_cluster_and_slow_tail() {
let fast = Duration::from_millis(1)..Duration::from_millis(2);
let slow = Duration::from_millis(50)..Duration::from_millis(100);
let dist = LatencyDistribution::Bimodal {
fast_range: fast.clone(),
slow_range: slow.clone(),
slow_probability: 0.05,
};
let values = samples(99, &dist, 10_000);
let fast_count = values.iter().filter(|d| fast.contains(d)).count();
let slow_count = values.iter().filter(|d| slow.contains(d)).count();
assert!(
fast_count > 8_000,
"expected a dominant fast cluster, got {fast_count}"
);
assert!(slow_count > 0, "expected a non-empty slow tail");
assert_eq!(fast_count + slow_count, values.len());
}
#[test]
fn exponential_with_zero_mean_returns_min() {
let dist = LatencyDistribution::Exponential {
min: Duration::from_micros(42),
mean: Duration::ZERO,
};
set_sim_seed(5);
for _ in 0..100 {
assert_eq!(sample_latency(&dist), Duration::from_micros(42));
}
}
#[test]
fn bimodal_probability_bounds_select_expected_range() {
let fast = Duration::from_millis(1)..Duration::from_millis(2);
let slow = Duration::from_millis(50)..Duration::from_millis(100);
let never_slow = LatencyDistribution::Bimodal {
fast_range: fast.clone(),
slow_range: slow.clone(),
slow_probability: 0.0,
};
let always_slow = LatencyDistribution::Bimodal {
fast_range: fast.clone(),
slow_range: slow.clone(),
slow_probability: 1.0,
};
for d in samples(1, &never_slow, 500) {
assert!(fast.contains(&d), "slow_probability 0.0 produced {d:?}");
}
for d in samples(2, &always_slow, 500) {
assert!(slow.contains(&d), "slow_probability 1.0 produced {d:?}");
}
}
#[test]
fn exponential_saturates_instead_of_panicking() {
let dist = LatencyDistribution::Exponential {
min: Duration::from_secs(1),
mean: Duration::from_secs(u64::MAX / 2),
};
set_sim_seed(3);
for _ in 0..1_000 {
let sampled = sample_latency(&dist);
assert!(sampled >= Duration::from_secs(1));
}
}
}