Skip to main content

mongreldb_sim/
rng.rs

1//! Seeded pseudo-random streams (spec section 9.5, FND-005).
2//!
3//! All nondeterminism in the simulator flows through [`SimRng`], a
4//! splitmix64 generator. splitmix64 is chosen because it is tiny, has
5//! no zero-state trap, and its counter structure makes stream splitting
6//! trivial: [`SimRng::fork`] and [`SimRng::fork_label`] hand each
7//! subsystem an independent stream, so drawing extra numbers in one
8//! subsystem never perturbs another.
9
10use serde::{Deserialize, Serialize};
11use std::fmt;
12use std::str::FromStr;
13
14const GAMMA: u64 = 0x9E37_79B9_7F4A_7C15;
15
16/// A reproducible seed for a simulation run.
17///
18/// The seed is the single input that fixes every interleaving, fault,
19/// and latency decision in a scenario. It is recorded in failure
20/// artifacts so a failing run can be replayed exactly.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct Seed(u64);
23
24impl Seed {
25    /// Wraps a raw 64-bit seed value.
26    pub fn new(value: u64) -> Self {
27        Self(value)
28    }
29
30    /// Returns the raw 64-bit seed value.
31    pub fn get(self) -> u64 {
32        self.0
33    }
34}
35
36impl fmt::Display for Seed {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "{}", self.0)
39    }
40}
41
42impl FromStr for Seed {
43    type Err = std::num::ParseIntError;
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        s.parse::<u64>().map(Self)
47    }
48}
49
50/// A splitmix64 pseudo-random stream.
51#[derive(Debug, Clone)]
52pub struct SimRng {
53    state: u64,
54}
55
56impl SimRng {
57    /// Creates a stream from a seed. A zero state is fine for splitmix64.
58    pub fn from_seed(seed: Seed) -> Self {
59        Self { state: seed.get() }
60    }
61
62    /// Advances the stream and returns the next 64-bit value.
63    pub fn next_u64(&mut self) -> u64 {
64        self.state = self.state.wrapping_add(GAMMA);
65        let mut z = self.state;
66        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
67        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
68        z ^ (z >> 31)
69    }
70
71    /// Uniform value in `0..n`, using the multiply-high trick to avoid
72    /// modulo bias. Panics if `n` is zero.
73    pub fn below(&mut self, n: u64) -> u64 {
74        assert!(n > 0, "SimRng::below requires n > 0");
75        ((self.next_u64() as u128 * n as u128) >> 64) as u64
76    }
77
78    /// Uniform value in `min..max` (half-open). Panics on an empty range.
79    pub fn range(&mut self, min: u64, max: u64) -> u64 {
80        assert!(min < max, "SimRng::range requires min < max");
81        min + self.below(max - min)
82    }
83
84    /// True with probability `per_mille / 1000`.
85    pub fn chance(&mut self, per_mille: u32) -> bool {
86        assert!(per_mille <= 1000, "per-mille probability out of range");
87        self.below(1000) < u64::from(per_mille)
88    }
89
90    /// Splits off an independent stream for a subsystem.
91    pub fn fork(&mut self) -> SimRng {
92        SimRng::from_seed(Seed(self.next_u64()))
93    }
94
95    /// Derives a named independent stream without advancing this one.
96    /// The label is mixed into the current state with FNV-1a.
97    pub fn fork_label(&self, label: &str) -> SimRng {
98        let mut hash = 0xcbf2_9ce4_8422_2325u64;
99        for byte in label.bytes() {
100            hash ^= u64::from(byte);
101            hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
102        }
103        let mut mixer = SimRng {
104            state: self.state ^ hash,
105        };
106        SimRng {
107            state: mixer.next_u64(),
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn same_seed_same_sequence() {
118        let mut a = SimRng::from_seed(Seed::new(42));
119        let mut b = SimRng::from_seed(Seed::new(42));
120        for _ in 0..100 {
121            assert_eq!(a.next_u64(), b.next_u64());
122        }
123    }
124
125    #[test]
126    fn different_seeds_diverge() {
127        let mut a = SimRng::from_seed(Seed::new(1));
128        let mut b = SimRng::from_seed(Seed::new(2));
129        assert_ne!(a.next_u64(), b.next_u64());
130    }
131
132    #[test]
133    fn fork_derives_child_from_one_parent_draw() {
134        // fork() consumes exactly one parent draw to seed the child;
135        // afterwards the two streams evolve independently.
136        let mut reference = SimRng::from_seed(Seed::new(7));
137        let draws: Vec<u64> = (0..10).map(|_| reference.next_u64()).collect();
138
139        let mut parent = SimRng::from_seed(Seed::new(7));
140        let mut observed = vec![parent.next_u64()];
141        let mut child = parent.fork();
142        observed.extend((0..8).map(|_| parent.next_u64()));
143
144        let mut expected = vec![draws[0]];
145        expected.extend_from_slice(&draws[2..]);
146        assert_eq!(observed, expected);
147
148        let mut seeded_child = SimRng::from_seed(Seed::new(draws[1]));
149        assert_eq!(child.next_u64(), seeded_child.next_u64());
150    }
151
152    #[test]
153    fn fork_label_is_stable_and_distinct() {
154        let rng = SimRng::from_seed(Seed::new(9));
155        assert_eq!(
156            rng.fork_label("net").next_u64(),
157            rng.fork_label("net").next_u64()
158        );
159        assert_ne!(
160            rng.fork_label("net").next_u64(),
161            rng.fork_label("disk").next_u64()
162        );
163    }
164
165    #[test]
166    fn ranges_and_chances_respect_bounds() {
167        let mut rng = SimRng::from_seed(Seed::new(3));
168        for _ in 0..1_000 {
169            assert!((5..10).contains(&rng.range(5, 10)));
170            assert!(rng.below(4) < 4);
171        }
172        assert!(rng.chance(1000));
173        assert!(!rng.chance(0));
174    }
175
176    #[test]
177    fn seed_serializes_for_repro_artifacts() {
178        let seed = Seed::new(123_456);
179        let json = serde_json::to_string(&seed).unwrap();
180        assert_eq!("123456", json);
181        let back: Seed = serde_json::from_str(&json).unwrap();
182        assert_eq!(seed, back);
183        assert_eq!("123456".parse::<Seed>().unwrap(), seed);
184    }
185}