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}