Skip to main content

rust_supervisor/policy/
backoff.rs

1//! Backoff timing for restart scheduling.
2//!
3//! This module owns exponential backoff calculation and deterministic jitter
4//! support. It does not sleep or spawn tasks.
5
6use serde::{Deserialize, Serialize};
7use std::time::Duration;
8
9/// Jitter source used by backoff calculation.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum JitterMode {
12    /// Adds no jitter and returns the exponential delay unchanged.
13    Disabled,
14    /// Adds deterministic jitter derived from this seed.
15    Deterministic {
16        /// Stable seed used by tests and reproducible simulations.
17        seed: u64,
18    },
19}
20
21/// Exponential backoff configuration for restart attempts.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23pub struct BackoffPolicy {
24    /// Initial delay for the first restart attempt.
25    pub initial: Duration,
26    /// Maximum delay allowed after exponential growth and jitter.
27    pub max: Duration,
28    /// Jitter percentage in the inclusive range from zero to one hundred.
29    pub jitter_percent: u8,
30    /// Stable runtime duration after which attempt counters may be reset.
31    pub reset_after: Duration,
32    /// Jitter mode used by the calculation.
33    pub jitter_mode: JitterMode,
34}
35
36impl BackoffPolicy {
37    /// Creates an exponential backoff policy.
38    ///
39    /// # Arguments
40    ///
41    /// - `initial`: First restart delay.
42    /// - `max`: Maximum restart delay.
43    /// - `jitter_percent`: Jitter percentage capped at one hundred.
44    /// - `reset_after`: Runtime duration after which counters may reset.
45    ///
46    /// # Returns
47    ///
48    /// Returns a [`BackoffPolicy`] with jitter disabled.
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use std::time::Duration;
54    ///
55    /// let policy = rust_supervisor::policy::backoff::BackoffPolicy::new(
56    ///     Duration::from_millis(10),
57    ///     Duration::from_millis(100),
58    ///     0,
59    ///     Duration::from_secs(1),
60    /// );
61    /// assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(10));
62    /// ```
63    pub fn new(
64        initial: Duration,
65        max: Duration,
66        jitter_percent: u8,
67        reset_after: Duration,
68    ) -> Self {
69        Self {
70            initial,
71            max,
72            jitter_percent: jitter_percent.min(100),
73            reset_after,
74            jitter_mode: JitterMode::Disabled,
75        }
76    }
77
78    /// Returns this policy with deterministic jitter enabled.
79    ///
80    /// # Arguments
81    ///
82    /// - `seed`: Stable seed used to derive jitter.
83    ///
84    /// # Returns
85    ///
86    /// Returns a new [`BackoffPolicy`] that keeps the same timing bounds.
87    pub fn with_deterministic_jitter(mut self, seed: u64) -> Self {
88        self.jitter_mode = JitterMode::Deterministic { seed };
89        self
90    }
91
92    /// Calculates a restart delay for a one-based attempt number.
93    ///
94    /// # Arguments
95    ///
96    /// - `attempt`: One-based restart attempt. Zero is treated as one.
97    ///
98    /// # Returns
99    ///
100    /// Returns a delay capped by [`BackoffPolicy::max`].
101    pub fn delay_for_attempt(&self, attempt: u64) -> Duration {
102        let exponential = self.exponential_delay(attempt.max(1));
103        self.apply_jitter(exponential).min(self.max)
104    }
105
106    /// Reports whether a stable runtime duration should reset counters.
107    ///
108    /// # Arguments
109    ///
110    /// - `stable_for`: Duration for which the child has run without failure.
111    ///
112    /// # Returns
113    ///
114    /// Returns `true` when `stable_for` reaches [`BackoffPolicy::reset_after`].
115    pub fn should_reset(&self, stable_for: Duration) -> bool {
116        stable_for >= self.reset_after
117    }
118
119    /// Computes the unclamped exponential delay.
120    ///
121    /// # Arguments
122    ///
123    /// - `attempt`: One-based restart attempt.
124    ///
125    /// # Returns
126    ///
127    /// Returns the exponential delay before jitter is applied.
128    fn exponential_delay(&self, attempt: u64) -> Duration {
129        let shift = attempt.saturating_sub(1).min(32);
130        let multiplier = 1_u128 << shift;
131        let millis = self.initial.as_millis().saturating_mul(multiplier);
132        duration_from_millis(millis).min(self.max)
133    }
134
135    /// Applies bounded jitter to a base delay.
136    ///
137    /// # Arguments
138    ///
139    /// - `base`: Delay before jitter.
140    ///
141    /// # Returns
142    ///
143    /// Returns a jittered delay that never exceeds the configured maximum.
144    fn apply_jitter(&self, base: Duration) -> Duration {
145        if self.jitter_percent == 0 {
146            return base;
147        }
148
149        match self.jitter_mode {
150            JitterMode::Disabled => base,
151            JitterMode::Deterministic { seed } => {
152                let jitter = deterministic_jitter(base, self.jitter_percent, seed);
153                base.saturating_add(jitter)
154            }
155        }
156    }
157}
158
159/// Converts milliseconds into a duration without overflowing.
160///
161/// # Arguments
162///
163/// - `millis`: Millisecond count held in a wide integer.
164///
165/// # Returns
166///
167/// Returns a [`Duration`] capped at `u64::MAX` milliseconds.
168fn duration_from_millis(millis: u128) -> Duration {
169    Duration::from_millis(millis.min(u64::MAX as u128) as u64)
170}
171
172/// Derives deterministic positive jitter.
173///
174/// # Arguments
175///
176/// - `base`: Base delay.
177/// - `percent`: Jitter percentage.
178/// - `seed`: Stable seed.
179///
180/// # Returns
181///
182/// Returns a jitter duration between zero and the configured percentage.
183fn deterministic_jitter(base: Duration, percent: u8, seed: u64) -> Duration {
184    let max_jitter = base.as_millis().saturating_mul(percent as u128) / 100;
185    if max_jitter == 0 {
186        return Duration::ZERO;
187    }
188
189    let mixed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
190    duration_from_millis((mixed as u128) % (max_jitter + 1))
191}