backoff.rs (5457B)
1 use core::time::Duration; 2 use serde::{Deserialize, Serialize}; 3 use std::time::{SystemTime, UNIX_EPOCH}; 4 5 fn default_base_ms() -> u64 { 6 500 7 } 8 9 fn default_max_ms() -> u64 { 10 30_000 11 } 12 13 fn default_factor() -> u32 { 14 2 15 } 16 17 fn default_jitter_ms() -> u64 { 18 0 19 } 20 21 #[derive(Debug, Clone, Serialize, Deserialize)] 22 #[serde(default, deny_unknown_fields)] 23 pub struct BackoffConfig { 24 #[serde(default = "default_base_ms")] 25 pub base_ms: u64, 26 #[serde(default = "default_max_ms")] 27 pub max_ms: u64, 28 #[serde(default = "default_factor")] 29 pub factor: u32, 30 #[serde(default = "default_jitter_ms")] 31 pub jitter_ms: u64, 32 } 33 34 impl Default for BackoffConfig { 35 fn default() -> Self { 36 Self { 37 base_ms: default_base_ms(), 38 max_ms: default_max_ms(), 39 factor: default_factor(), 40 jitter_ms: default_jitter_ms(), 41 } 42 } 43 } 44 45 impl BackoffConfig { 46 pub fn delay_for_attempt(&self, attempt: u32) -> Duration { 47 let base = self.base_ms.max(1); 48 let max = self.max_ms.max(base); 49 let factor = self.factor.max(1) as u64; 50 51 let mut delay = base; 52 let steps = attempt.saturating_sub(1).min(10); 53 for _ in 0..steps { 54 delay = delay.saturating_mul(factor).min(max); 55 } 56 57 if self.jitter_ms > 0 { 58 let jitter = jitter_ms(self.jitter_ms); 59 delay = delay.saturating_add(jitter).min(max); 60 } 61 62 Duration::from_millis(delay) 63 } 64 } 65 66 #[derive(Debug, Clone)] 67 pub struct Backoff { 68 cfg: BackoffConfig, 69 attempt: u32, 70 } 71 72 impl Backoff { 73 pub fn new(cfg: BackoffConfig) -> Self { 74 Self { cfg, attempt: 0 } 75 } 76 77 pub fn reset(&mut self) { 78 self.attempt = 0; 79 } 80 81 pub fn next_delay(&mut self) -> Duration { 82 let attempt = self.attempt.saturating_add(1); 83 self.attempt = attempt; 84 self.cfg.delay_for_attempt(attempt) 85 } 86 87 pub fn attempt(&self) -> u32 { 88 self.attempt 89 } 90 } 91 92 fn jitter_ms(max: u64) -> u64 { 93 if max == 0 { 94 return 0; 95 } 96 let nanos = SystemTime::now() 97 .duration_since(UNIX_EPOCH) 98 .unwrap_or_default() 99 .subsec_nanos() as u64; 100 nanos % (max + 1) 101 } 102 103 #[cfg(test)] 104 mod tests { 105 use super::{Backoff, BackoffConfig, jitter_ms}; 106 use core::time::Duration; 107 108 #[test] 109 fn default_values_round_trip() { 110 let cfg: BackoffConfig = 111 toml::from_str("").expect("backoff config defaults should deserialize"); 112 assert_eq!(cfg.base_ms, 500); 113 assert_eq!(cfg.max_ms, 30_000); 114 assert_eq!(cfg.factor, 2); 115 assert_eq!(cfg.jitter_ms, 0); 116 117 let cfg_default = BackoffConfig::default(); 118 assert_eq!(cfg_default.base_ms, 500); 119 assert_eq!(cfg_default.max_ms, 30_000); 120 assert_eq!(cfg_default.factor, 2); 121 assert_eq!(cfg_default.jitter_ms, 0); 122 } 123 124 #[test] 125 fn reconnect_alias_fields_are_rejected() { 126 let err = toml::from_str::<BackoffConfig>( 127 r#" 128 reconnect_base_ms = 10 129 reconnect_max_ms = 100 130 reconnect_factor = 3 131 reconnect_jitter_ms = 5 132 "#, 133 ) 134 .expect_err("backoff aliases should fail"); 135 136 assert!(err.to_string().contains("reconnect_base_ms")); 137 } 138 139 #[test] 140 fn delay_for_attempt_applies_bounds_and_factor_defaults() { 141 let cfg = BackoffConfig { 142 base_ms: 0, 143 max_ms: 0, 144 factor: 0, 145 jitter_ms: 0, 146 }; 147 assert_eq!(cfg.delay_for_attempt(1), Duration::from_millis(1)); 148 assert_eq!(cfg.delay_for_attempt(8), Duration::from_millis(1)); 149 } 150 151 #[test] 152 fn delay_for_attempt_caps_growth_to_max() { 153 let cfg = BackoffConfig { 154 base_ms: 100, 155 max_ms: 1_000, 156 factor: 2, 157 jitter_ms: 0, 158 }; 159 160 assert_eq!(cfg.delay_for_attempt(1), Duration::from_millis(100)); 161 assert_eq!(cfg.delay_for_attempt(2), Duration::from_millis(200)); 162 assert_eq!(cfg.delay_for_attempt(3), Duration::from_millis(400)); 163 assert_eq!(cfg.delay_for_attempt(4), Duration::from_millis(800)); 164 assert_eq!(cfg.delay_for_attempt(5), Duration::from_millis(1_000)); 165 assert_eq!(cfg.delay_for_attempt(16), Duration::from_millis(1_000)); 166 } 167 168 #[test] 169 fn delay_for_attempt_applies_jitter_without_exceeding_max() { 170 let cfg = BackoffConfig { 171 base_ms: 100, 172 max_ms: 500, 173 factor: 2, 174 jitter_ms: 50, 175 }; 176 177 let delay = cfg.delay_for_attempt(2).as_millis() as u64; 178 assert!(delay >= 200); 179 assert!(delay <= 250); 180 } 181 182 #[test] 183 fn stateful_backoff_tracks_attempts_and_reset() { 184 let cfg = BackoffConfig { 185 base_ms: 5, 186 max_ms: 50, 187 factor: 2, 188 jitter_ms: 0, 189 }; 190 let mut backoff = Backoff::new(cfg); 191 192 assert_eq!(backoff.attempt(), 0); 193 assert_eq!(backoff.next_delay(), Duration::from_millis(5)); 194 assert_eq!(backoff.attempt(), 1); 195 assert_eq!(backoff.next_delay(), Duration::from_millis(10)); 196 assert_eq!(backoff.attempt(), 2); 197 198 backoff.reset(); 199 assert_eq!(backoff.attempt(), 0); 200 assert_eq!(backoff.next_delay(), Duration::from_millis(5)); 201 } 202 203 #[test] 204 fn jitter_ms_bounds_output() { 205 assert_eq!(jitter_ms(0), 0); 206 let jitter = jitter_ms(7); 207 assert!(jitter <= 7); 208 } 209 }