lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }