1use std::{future::Future, pin::Pin, time::Duration};
2
3use runifold_core::RetrySafety;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::{ModelError, ModelErrorKind};
8
9pub type RouterSleepFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
11
12pub trait RouterSleeper: Send + Sync {
14 fn sleep(&self, duration: Duration) -> RouterSleepFuture<'_>;
16}
17
18#[derive(Clone, Copy, Debug, Default)]
20pub struct SystemRouterSleeper;
21
22impl RouterSleeper for SystemRouterSleeper {
23 fn sleep(&self, duration: Duration) -> RouterSleepFuture<'_> {
24 Box::pin(futures_timer::Delay::new(duration))
25 }
26}
27
28#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
30#[serde(rename_all = "snake_case")]
31#[non_exhaustive]
32pub enum RetryJitter {
33 None,
35 #[default]
38 Full,
39}
40
41#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
43#[non_exhaustive]
44pub enum ModelRetryPolicyError {
45 #[error("model retry max_attempts must be greater than zero")]
47 ZeroMaxAttempts,
48 #[error("model retry backoff multiplier must be greater than zero")]
50 ZeroMultiplier,
51 #[error("model retry max_backoff cannot be less than initial_backoff")]
53 InvalidBackoffRange,
54}
55
56#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct ModelRetryPolicy {
59 max_attempts: u32,
60 initial_backoff: Duration,
61 max_backoff: Duration,
62 multiplier: u32,
63 jitter: RetryJitter,
64 unknown_safety_kinds: Vec<ModelErrorKind>,
65}
66
67impl Default for ModelRetryPolicy {
68 fn default() -> Self {
69 Self {
70 max_attempts: 3,
71 initial_backoff: Duration::from_millis(100),
72 max_backoff: Duration::from_secs(2),
73 multiplier: 2,
74 jitter: RetryJitter::Full,
75 unknown_safety_kinds: Vec::new(),
76 }
77 }
78}
79
80impl ModelRetryPolicy {
81 pub fn exponential(
88 max_attempts: u32,
89 initial_backoff: Duration,
90 max_backoff: Duration,
91 multiplier: u32,
92 ) -> Result<Self, ModelRetryPolicyError> {
93 if max_attempts == 0 {
94 return Err(ModelRetryPolicyError::ZeroMaxAttempts);
95 }
96 if multiplier == 0 {
97 return Err(ModelRetryPolicyError::ZeroMultiplier);
98 }
99 if max_backoff < initial_backoff {
100 return Err(ModelRetryPolicyError::InvalidBackoffRange);
101 }
102 Ok(Self {
103 max_attempts,
104 initial_backoff,
105 max_backoff,
106 multiplier,
107 jitter: RetryJitter::Full,
108 unknown_safety_kinds: Vec::new(),
109 })
110 }
111
112 #[must_use]
114 pub const fn jitter(mut self, jitter: RetryJitter) -> Self {
115 self.jitter = jitter;
116 self
117 }
118
119 #[must_use]
124 pub fn allow_unknown(mut self, kind: ModelErrorKind) -> Self {
125 if !self.unknown_safety_kinds.contains(&kind) {
126 self.unknown_safety_kinds.push(kind);
127 }
128 self
129 }
130
131 pub const fn max_attempts(&self) -> u32 {
133 self.max_attempts
134 }
135
136 pub const fn initial_backoff(&self) -> Duration {
138 self.initial_backoff
139 }
140
141 pub const fn max_backoff(&self) -> Duration {
143 self.max_backoff
144 }
145
146 pub const fn multiplier(&self) -> u32 {
148 self.multiplier
149 }
150
151 pub const fn jitter_mode(&self) -> RetryJitter {
153 self.jitter
154 }
155
156 pub(crate) fn permits(&self, error: &ModelError) -> bool {
157 if error.kind == ModelErrorKind::Cancelled {
158 return false;
159 }
160 match error.retry_safety {
161 RetrySafety::Safe => true,
162 RetrySafety::Unknown => self.unknown_safety_kinds.contains(&error.kind),
163 _ => false,
164 }
165 }
166
167 pub(crate) fn delay(&self, retry: u32, entropy: u64) -> Duration {
168 let exponent = retry.saturating_sub(1);
169 let mut delay = self.initial_backoff;
170 for _ in 0..exponent {
171 if delay >= self.max_backoff {
172 break;
173 }
174 delay = delay
175 .checked_mul(self.multiplier)
176 .unwrap_or(self.max_backoff)
177 .min(self.max_backoff);
178 }
179 match self.jitter {
180 RetryJitter::None => delay,
181 RetryJitter::Full => full_jitter(delay, entropy),
182 }
183 }
184}
185
186fn full_jitter(cap: Duration, entropy: u64) -> Duration {
187 let cap_nanos = u64::try_from(cap.as_nanos()).unwrap_or(u64::MAX);
188 if cap_nanos == u64::MAX {
189 return Duration::from_nanos(entropy);
190 }
191 Duration::from_nanos(entropy % cap_nanos.saturating_add(1))
192}
193
194#[cfg(test)]
195mod tests {
196 use std::time::Duration;
197
198 use crate::{ModelError, ModelErrorKind};
199 use runifold_core::RetrySafety;
200
201 use super::{ModelRetryPolicy, ModelRetryPolicyError, RetryJitter};
202
203 #[test]
204 fn exponential_delay_is_capped_without_overflow() {
205 let policy = ModelRetryPolicy::exponential(
206 10,
207 Duration::from_millis(100),
208 Duration::from_secs(1),
209 3,
210 )
211 .unwrap()
212 .jitter(RetryJitter::None);
213
214 assert_eq!(policy.delay(1, 0), Duration::from_millis(100));
215 assert_eq!(policy.delay(2, 0), Duration::from_millis(300));
216 assert_eq!(policy.delay(3, 0), Duration::from_millis(900));
217 assert_eq!(policy.delay(4, 0), Duration::from_secs(1));
218 assert_eq!(policy.delay(u32::MAX, 0), Duration::from_secs(1));
219 }
220
221 #[test]
222 fn full_jitter_is_deterministic_and_within_cap() {
223 let policy = ModelRetryPolicy::exponential(
224 2,
225 Duration::from_millis(100),
226 Duration::from_millis(100),
227 2,
228 )
229 .unwrap();
230
231 let first = policy.delay(1, 42);
232 let second = policy.delay(1, 42);
233 assert_eq!(first, second);
234 assert!(first <= Duration::from_millis(100));
235 }
236
237 #[test]
238 fn invalid_policy_is_rejected() {
239 assert_eq!(
240 ModelRetryPolicy::exponential(
241 0,
242 Duration::from_millis(1),
243 Duration::from_millis(1),
244 2,
245 )
246 .unwrap_err(),
247 ModelRetryPolicyError::ZeroMaxAttempts
248 );
249 }
250
251 #[test]
252 fn default_policy_is_bounded_and_safe_only() {
253 let policy = ModelRetryPolicy::default();
254
255 assert_eq!(policy.max_attempts(), 3);
256 assert_eq!(policy.initial_backoff(), Duration::from_millis(100));
257 assert_eq!(policy.max_backoff(), Duration::from_secs(2));
258 assert_eq!(policy.multiplier(), 2);
259 assert_eq!(policy.jitter_mode(), RetryJitter::Full);
260 }
261
262 #[test]
263 fn unknown_error_requires_explicit_retry_authority() {
264 let policy = ModelRetryPolicy::exponential(2, Duration::ZERO, Duration::ZERO, 1).unwrap();
265 let mut error = ModelError::local(ModelErrorKind::Transport, "failure");
266 error.retry_safety = RetrySafety::Unknown;
267 assert!(!policy.permits(&error));
268 assert!(
269 policy
270 .allow_unknown(ModelErrorKind::Transport)
271 .permits(&error)
272 );
273 }
274}