Skip to main content

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}