Skip to main content

moonpool_sim/chaos/
buggify.rs

1//! Deterministic fault injection following `FoundationDB`'s buggify approach.
2//!
3//! Each buggify location is randomly activated once per simulation run.
4//! Active locations fire probabilistically on each call.
5
6use crate::sim::rng::sim_random;
7use rand::distr::uniform::SampleUniform;
8use std::cell::RefCell;
9use std::collections::BTreeMap;
10
11thread_local! {
12    static STATE: RefCell<State> = RefCell::new(State::default());
13}
14
15#[derive(Default)]
16struct State {
17    enabled: bool,
18    active_locations: BTreeMap<String, bool>,
19    activation_prob: f64,
20}
21
22/// Initialize buggify for simulation run
23pub fn buggify_init(activation_prob: f64, _firing_prob: f64) {
24    STATE.with(|state| {
25        let mut state = state.borrow_mut();
26        state.enabled = true;
27        state.active_locations.clear();
28        state.activation_prob = activation_prob;
29    });
30}
31
32/// Reset/disable buggify
33pub fn buggify_reset() {
34    STATE.with(|state| {
35        let mut state = state.borrow_mut();
36        state.enabled = false;
37        state.active_locations.clear();
38        state.activation_prob = 0.0;
39    });
40}
41
42/// Internal buggify implementation
43#[must_use]
44pub fn buggify_internal(prob: f64, location: &'static str) -> bool {
45    STATE.with(|state| {
46        let mut state = state.borrow_mut();
47
48        if !state.enabled || prob <= 0.0 {
49            return false;
50        }
51
52        let location_str = location.to_string();
53        let activation_prob = state.activation_prob;
54
55        // Decide activation on first encounter
56        let is_active = *state
57            .active_locations
58            .entry(location_str)
59            .or_insert_with(|| sim_random::<f64>() < activation_prob);
60
61        // If active, fire probabilistically
62        is_active && sim_random::<f64>() < prob
63    })
64}
65
66/// Buggify a knob *value* within bounds.
67///
68/// Returns `default` on most seeds, but when this call site is buggify-activated
69/// and fires (same two-phase model as [`buggify_internal`]) it returns a random
70/// value drawn from `range`. Deterministic per `(location, seed)`; varies across
71/// seeds. Mirrors `FoundationDB`'s `if (randomize && BUGGIFY) KNOB = random(lo, hi)`
72/// (`Buggify.h`): a knob keeps its configured value most of the time, but a given
73/// seed occasionally spikes it to an extreme within `range`.
74#[must_use]
75pub fn buggify_knob_internal<T>(default: T, range: std::ops::Range<T>, location: &'static str) -> T
76where
77    T: SampleUniform + PartialOrd + Clone,
78{
79    if buggify_internal(0.25, location) {
80        crate::sim::sim_random_range_or_default(range)
81    } else {
82        default
83    }
84}
85
86/// Buggify with 25% probability
87#[macro_export]
88macro_rules! buggify {
89    () => {
90        $crate::chaos::buggify::buggify_internal(0.25, concat!(file!(), ":", line!()))
91    };
92}
93
94/// Buggify with custom probability
95#[macro_export]
96macro_rules! buggify_with_prob {
97    ($prob:expr) => {
98        $crate::chaos::buggify::buggify_internal($prob as f64, concat!(file!(), ":", line!()))
99    };
100}
101
102/// Buggify a config knob *value* within bounds.
103///
104/// `buggify_knob!(default, lo..hi)` evaluates to `default` on most seeds, but when
105/// buggify is enabled and this call site is activated + fires (same model as
106/// [`buggify!`]) it evaluates to a random value in `lo..hi`. Deterministic per
107/// `(location, seed)`, so replay is exact. Mirrors `FoundationDB`'s
108/// `if (randomize && BUGGIFY) KNOB = random(lo, hi)`.
109#[macro_export]
110macro_rules! buggify_knob {
111    ($default:expr, $range:expr) => {
112        $crate::chaos::buggify::buggify_knob_internal(
113            $default,
114            $range,
115            concat!(file!(), ":", line!()),
116        )
117    };
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::sim::rng::{reset_sim_rng, set_sim_seed};
124
125    #[test]
126    fn test_disabled_by_default() {
127        buggify_reset();
128        for _ in 0..10 {
129            assert!(!buggify_internal(1.0, "test"));
130        }
131    }
132
133    #[test]
134    fn test_activation_consistency() {
135        set_sim_seed(12345);
136        buggify_init(0.5, 1.0);
137
138        let location = "test_location";
139        let first = buggify_internal(1.0, location);
140        let second = buggify_internal(1.0, location);
141
142        // Activation decision should be consistent
143        assert_eq!(first, second);
144        buggify_reset();
145    }
146
147    #[test]
148    fn test_deterministic() {
149        const SEED: u64 = 54321;
150        let mut results1 = Vec::new();
151        let mut results2 = Vec::new();
152
153        for run in 0..2 {
154            set_sim_seed(SEED);
155            buggify_init(0.5, 0.5);
156
157            let results = if run == 0 {
158                &mut results1
159            } else {
160                &mut results2
161            };
162
163            for i in 0..5 {
164                let location = format!("loc_{i}");
165                results.push(buggify_internal(0.5, Box::leak(location.into_boxed_str())));
166            }
167
168            buggify_reset();
169            reset_sim_rng();
170        }
171
172        assert_eq!(results1, results2);
173    }
174
175    /// Collect a fixed sequence of `buggify_knob!` results for one seed.
176    fn knob_sequence(seed: u64) -> Vec<u64> {
177        reset_sim_rng();
178        set_sim_seed(seed);
179        buggify_init(0.8, 0.8);
180        let mut out = Vec::new();
181        for i in 0..20 {
182            // Distinct call-site identity per index without a real source line.
183            let location = Box::leak(format!("knob_{i}").into_boxed_str());
184            out.push(buggify_knob_internal::<u64>(100, 1_000..2_000, location));
185        }
186        buggify_reset();
187        out
188    }
189
190    #[test]
191    fn test_buggify_knob_deterministic() {
192        const SEED: u64 = 98765;
193        assert_eq!(knob_sequence(SEED), knob_sequence(SEED));
194    }
195
196    #[test]
197    fn test_buggify_knob_varies_across_seeds() {
198        let sequences: Vec<Vec<u64>> = [111u64, 222, 333, 444, 555]
199            .iter()
200            .map(|s| knob_sequence(*s))
201            .collect();
202        let unique = sequences
203            .iter()
204            .collect::<std::collections::HashSet<_>>()
205            .len();
206        assert!(
207            unique > 1,
208            "different seeds should yield different knob spikes"
209        );
210    }
211
212    #[test]
213    fn test_buggify_knob_disabled_returns_default() {
214        reset_sim_rng();
215        set_sim_seed(42);
216        buggify_reset(); // disabled: never spike
217        for _ in 0..10 {
218            assert_eq!(buggify_knob_internal::<u64>(100, 1_000..2_000, "loc"), 100);
219        }
220    }
221
222    #[test]
223    fn test_buggify_knob_spiked_value_in_range() {
224        reset_sim_rng();
225        set_sim_seed(7);
226        buggify_init(1.0, 1.0); // always active + fire
227        let v = buggify_knob_internal::<u64>(100, 1_000..2_000, "always");
228        buggify_reset();
229        assert!(
230            (1_000..2_000).contains(&v),
231            "spiked value must be in range, got {v}"
232        );
233    }
234}