Skip to main content

dynomite/hashkit/
random.rs

1//! Pseudo-random integer source used by the `random` distribution.
2//!
3//! Requests to live servers are dispatched using `random_index() %
4//! ncontinuum`, backed by a small, deterministic-when-seeded LCG so
5//! the engine does not depend on libc's BSD `random()` family.
6//!
7//! The default seed is drawn from `SystemTime::now()` (nanoseconds
8//! since the Unix epoch), which gives per-process seed entropy
9//! without requiring the `time` cargo feature on the `nix`
10//! workspace dependency. The choice is documented as a deviation in
11//! `docs/parity.md` and pinned by a regression test, so any future
12//! move to a monotonic source is an intentional change rather than a
13//! drift.
14
15use std::time::{SystemTime, UNIX_EPOCH};
16
17/// Linear congruential generator with the parameters used by glibc's
18/// non-secure `random()` family. Sufficient for ring dispatch (the only
19/// caller); not cryptographically strong.
20///
21/// # Examples
22///
23/// ```
24/// use dynomite::hashkit::PseudoRng;
25/// let mut rng = PseudoRng::from_seed(42);
26/// let value = rng.next_u32();
27/// // Same seed reproduces the same stream.
28/// let mut rng2 = PseudoRng::from_seed(42);
29/// assert_eq!(value, rng2.next_u32());
30/// ```
31#[derive(Clone, Debug)]
32pub struct PseudoRng {
33    state: u64,
34}
35
36impl PseudoRng {
37    /// Construct a generator seeded from the system clock.
38    ///
39    /// The seed mixes seconds and nanoseconds drawn from
40    /// [`SystemTime::now()`]. See the module-level docs for why we use
41    /// the system clock rather than a monotonic clock.
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// use dynomite::hashkit::PseudoRng;
47    /// let mut rng = PseudoRng::from_monotonic();
48    /// let _ = rng.next_u32();
49    /// ```
50    #[must_use]
51    pub fn from_monotonic() -> Self {
52        let seed = clock_seed();
53        Self::from_seed(seed)
54    }
55
56    /// Construct a generator from an explicit seed.
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use dynomite::hashkit::PseudoRng;
62    /// let mut a = PseudoRng::from_seed(7);
63    /// let mut b = PseudoRng::from_seed(7);
64    /// assert_eq!(a.next_u32(), b.next_u32());
65    /// ```
66    #[must_use]
67    pub fn from_seed(seed: u64) -> Self {
68        // A zero seed in the LCG is a degenerate fixed point; nudge it.
69        let s = if seed == 0 {
70            0x9E37_79B9_7F4A_7C15
71        } else {
72            seed
73        };
74        Self { state: s }
75    }
76
77    /// Advance the generator and return the next 32-bit value.
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use dynomite::hashkit::PseudoRng;
83    /// let mut rng = PseudoRng::from_seed(1);
84    /// let _: u32 = rng.next_u32();
85    /// ```
86    pub fn next_u32(&mut self) -> u32 {
87        // Knuth's MMIX LCG parameters; long-period and full 64-bit state.
88        self.state = self
89            .state
90            .wrapping_mul(6_364_136_223_846_793_005)
91            .wrapping_add(1_442_695_040_888_963_407);
92        // High bits of the LCG state have better statistical properties
93        // than the low bits, so return the upper half.
94        (self.state >> 32) as u32
95    }
96
97    /// Pick a uniform value in `[0, modulus)`. Returns `0` when modulus
98    /// is zero.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use dynomite::hashkit::PseudoRng;
104    /// let mut rng = PseudoRng::from_seed(7);
105    /// for _ in 0..16 {
106    ///     assert!(rng.next_index(13) < 13);
107    /// }
108    /// assert_eq!(rng.next_index(0), 0);
109    /// ```
110    pub fn next_index(&mut self, modulus: u32) -> u32 {
111        if modulus == 0 {
112            0
113        } else {
114            self.next_u32() % modulus
115        }
116    }
117}
118
119fn clock_seed() -> u64 {
120    SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| {
121        let secs = d.as_secs();
122        let nanos = u64::from(d.subsec_nanos());
123        secs.wrapping_mul(1_000_000_000).wrapping_add(nanos)
124    })
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn deterministic_for_same_seed() {
133        let mut a = PseudoRng::from_seed(123);
134        let mut b = PseudoRng::from_seed(123);
135        for _ in 0..32 {
136            assert_eq!(a.next_u32(), b.next_u32());
137        }
138    }
139
140    /// Pins the choice of LCG parameters and the high-bit return
141    /// strategy. If this test breaks, treat it as an intentional API
142    /// change and update `docs/parity.md`'s `PseudoRng` deviation.
143    #[test]
144    fn lcg_parameters_are_pinned() {
145        // Knuth MMIX constants applied once and the high 32 bits of
146        // the resulting state.
147        let mut rng = PseudoRng::from_seed(1);
148        let expected = ((1u64
149            .wrapping_mul(6_364_136_223_846_793_005)
150            .wrapping_add(1_442_695_040_888_963_407))
151            >> 32) as u32;
152        assert_eq!(rng.next_u32(), expected);
153    }
154
155    #[test]
156    fn distinct_seeds_diverge() {
157        let mut a = PseudoRng::from_seed(1);
158        let mut b = PseudoRng::from_seed(2);
159        let av: Vec<u32> = (0..16).map(|_| a.next_u32()).collect();
160        let bv: Vec<u32> = (0..16).map(|_| b.next_u32()).collect();
161        assert_ne!(av, bv);
162    }
163
164    #[test]
165    fn next_index_bounded() {
166        let mut rng = PseudoRng::from_seed(42);
167        for _ in 0..256 {
168            let i = rng.next_index(13);
169            assert!(i < 13);
170        }
171    }
172
173    #[test]
174    fn next_index_zero_modulus() {
175        let mut rng = PseudoRng::from_seed(42);
176        assert_eq!(rng.next_index(0), 0);
177    }
178
179    #[test]
180    fn monotonic_seed_produces_values() {
181        let mut rng = PseudoRng::from_monotonic();
182        let _ = rng.next_u32();
183    }
184}