connpool/rng.rs
1//! A tiny, dependency-free random source for jitter (retry backoff and lifetime jitter).
2//!
3//! The library never needs cryptographic randomness — only uniform `f64`s in `[0, 1)` to
4//! spread retries and connection lifetimes. Rather than pull in the `rand` crate (and break
5//! the zero-runtime-dependency rule), we ship a small [SplitMix64] generator seeded from the
6//! standard library's process-randomized [`RandomState`], the same entropy source `HashMap`
7//! uses. Tests inject a deterministic seed for reproducibility.
8//!
9//! [SplitMix64]: https://prng.di.unimi.it/splitmix64.c
10//! [`RandomState`]: std::collections::hash_map::RandomState
11
12use std::collections::hash_map::RandomState;
13use std::hash::{BuildHasher, Hasher};
14
15/// A source of uniform `f64`s in `[0, 1)`. Mirrors `random.Random.random()` /
16/// `rand.Rand.Float64()` from the sibling implementations.
17pub trait Rng: Send {
18 /// Returns the next uniform value in `[0, 1)`.
19 fn next_f64(&mut self) -> f64;
20}
21
22/// The default [`Rng`]: a SplitMix64 PRNG seeded from the standard library's
23/// process-randomized hasher state. Non-cryptographic — it exists only to jitter timings.
24#[derive(Debug, Clone)]
25pub struct SystemRng {
26 state: u64,
27}
28
29impl SystemRng {
30 /// Builds a generator with a fresh, process-randomized seed.
31 pub fn new() -> Self {
32 // RandomState is seeded from OS entropy per process; hashing nothing still yields a
33 // random finish() because the keys themselves are random.
34 let seed = RandomState::new().build_hasher().finish();
35 Self::with_seed(seed)
36 }
37
38 /// Builds a generator from an explicit seed (deterministic; used in tests).
39 pub fn with_seed(seed: u64) -> Self {
40 Self { state: seed }
41 }
42
43 fn next_u64(&mut self) -> u64 {
44 // SplitMix64.
45 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
46 let mut z = self.state;
47 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
48 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
49 z ^ (z >> 31)
50 }
51}
52
53impl Default for SystemRng {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl Rng for SystemRng {
60 fn next_f64(&mut self) -> f64 {
61 // Take the top 53 bits for a uniform double in [0, 1), the usual construction.
62 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn values_are_in_unit_interval() {
72 let mut rng = SystemRng::with_seed(42);
73 for _ in 0..10_000 {
74 let v = rng.next_f64();
75 assert!((0.0..1.0).contains(&v), "value out of range: {v}");
76 }
77 }
78
79 #[test]
80 fn seeded_is_deterministic_and_default_runs() {
81 let mut a = SystemRng::with_seed(123);
82 let mut b = SystemRng::with_seed(123);
83 for _ in 0..100 {
84 assert_eq!(a.next_f64(), b.next_f64());
85 }
86 // A differently-seeded stream should diverge.
87 let mut c = SystemRng::with_seed(124);
88 assert_ne!(a.next_f64(), c.next_f64());
89 // Exercise the process-seeded constructors (Default + new).
90 let mut d = SystemRng::default();
91 let _ = d.next_f64();
92 let mut e = SystemRng::new();
93 let _ = e.next_f64();
94 // Debug/Clone are derived; touch them so they're covered.
95 let cloned = a.clone();
96 assert_eq!(format!("{cloned:?}"), format!("{a:?}"));
97 }
98}