1use std::{
2 collections::hash_map::DefaultHasher,
3 hash::{Hash, Hasher},
4 time::Duration,
5};
6
7pub fn jitter_ttl(base: Duration, ratio: f64, seed: impl Hash) -> Duration {
12 if base.is_zero() {
13 return base;
14 }
15
16 let ratio = ratio.clamp(0.0, 1.0);
17 if ratio == 0.0 {
18 return base;
19 }
20
21 let mut hasher = DefaultHasher::new();
22 seed.hash(&mut hasher);
23 let bucket = (hasher.finish() % 10_001) as f64 / 10_000.0;
24 let factor = 1.0 - ratio + bucket * ratio * 2.0;
25 Duration::from_secs_f64((base.as_secs_f64() * factor).max(0.001))
26}
27
28#[cfg(test)]
29mod tests {
30 use std::time::Duration;
31
32 use super::jitter_ttl;
33
34 #[test]
35 fn jitter_stays_inside_ratio_bounds() {
36 let ttl = jitter_ttl(Duration::from_secs(100), 0.05, "user:1");
37 assert!(ttl >= Duration::from_secs(95));
38 assert!(ttl <= Duration::from_secs(105));
39 }
40
41 #[test]
42 fn zero_ratio_keeps_original_ttl() {
43 let ttl = Duration::from_secs(60);
44 assert_eq!(jitter_ttl(ttl, 0.0, "key"), ttl);
45 }
46}