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