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}