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 ModelRetryPolicy {
68 pub fn exponential(
75 max_attempts: u32,
76 initial_backoff: Duration,
77 max_backoff: Duration,
78 multiplier: u32,
79 ) -> Result<Self, ModelRetryPolicyError> {
80 if max_attempts == 0 {
81 return Err(ModelRetryPolicyError::ZeroMaxAttempts);
82 }
83 if multiplier == 0 {
84 return Err(ModelRetryPolicyError::ZeroMultiplier);
85 }
86 if max_backoff < initial_backoff {
87 return Err(ModelRetryPolicyError::InvalidBackoffRange);
88 }
89 Ok(Self {
90 max_attempts,
91 initial_backoff,
92 max_backoff,
93 multiplier,
94 jitter: RetryJitter::Full,
95 unknown_safety_kinds: Vec::new(),
96 })
97 }
98
99 #[must_use]
101 pub const fn jitter(mut self, jitter: RetryJitter) -> Self {
102 self.jitter = jitter;
103 self
104 }
105
106 #[must_use]
111 pub fn allow_unknown(mut self, kind: ModelErrorKind) -> Self {
112 if !self.unknown_safety_kinds.contains(&kind) {
113 self.unknown_safety_kinds.push(kind);
114 }
115 self
116 }
117
118 pub const fn max_attempts(&self) -> u32 {
120 self.max_attempts
121 }
122
123 pub const fn initial_backoff(&self) -> Duration {
125 self.initial_backoff
126 }
127
128 pub const fn max_backoff(&self) -> Duration {
130 self.max_backoff
131 }
132
133 pub const fn multiplier(&self) -> u32 {
135 self.multiplier
136 }
137
138 pub const fn jitter_mode(&self) -> RetryJitter {
140 self.jitter
141 }
142
143 pub(crate) fn permits(&self, error: &ModelError) -> bool {
144 if error.kind == ModelErrorKind::Cancelled {
145 return false;
146 }
147 match error.retry_safety {
148 RetrySafety::Safe => true,
149 RetrySafety::Unknown => self.unknown_safety_kinds.contains(&error.kind),
150 _ => false,
151 }
152 }
153
154 pub(crate) fn delay(&self, retry: u32, entropy: u64) -> Duration {
155 let exponent = retry.saturating_sub(1);
156 let mut delay = self.initial_backoff;
157 for _ in 0..exponent {
158 if delay >= self.max_backoff {
159 break;
160 }
161 delay = delay
162 .checked_mul(self.multiplier)
163 .unwrap_or(self.max_backoff)
164 .min(self.max_backoff);
165 }
166 match self.jitter {
167 RetryJitter::None => delay,
168 RetryJitter::Full => full_jitter(delay, entropy),
169 }
170 }
171}
172
173fn full_jitter(cap: Duration, entropy: u64) -> Duration {
174 let cap_nanos = u64::try_from(cap.as_nanos()).unwrap_or(u64::MAX);
175 if cap_nanos == u64::MAX {
176 return Duration::from_nanos(entropy);
177 }
178 Duration::from_nanos(entropy % cap_nanos.saturating_add(1))
179}
180
181#[cfg(test)]
182mod tests {
183 use std::time::Duration;
184
185 use crate::{ModelError, ModelErrorKind};
186 use runifold_core::RetrySafety;
187
188 use super::{ModelRetryPolicy, ModelRetryPolicyError, RetryJitter};
189
190 #[test]
191 fn exponential_delay_is_capped_without_overflow() {
192 let policy = ModelRetryPolicy::exponential(
193 10,
194 Duration::from_millis(100),
195 Duration::from_secs(1),
196 3,
197 )
198 .unwrap()
199 .jitter(RetryJitter::None);
200
201 assert_eq!(policy.delay(1, 0), Duration::from_millis(100));
202 assert_eq!(policy.delay(2, 0), Duration::from_millis(300));
203 assert_eq!(policy.delay(3, 0), Duration::from_millis(900));
204 assert_eq!(policy.delay(4, 0), Duration::from_secs(1));
205 assert_eq!(policy.delay(u32::MAX, 0), Duration::from_secs(1));
206 }
207
208 #[test]
209 fn full_jitter_is_deterministic_and_within_cap() {
210 let policy = ModelRetryPolicy::exponential(
211 2,
212 Duration::from_millis(100),
213 Duration::from_millis(100),
214 2,
215 )
216 .unwrap();
217
218 let first = policy.delay(1, 42);
219 let second = policy.delay(1, 42);
220 assert_eq!(first, second);
221 assert!(first <= Duration::from_millis(100));
222 }
223
224 #[test]
225 fn invalid_policy_is_rejected() {
226 assert_eq!(
227 ModelRetryPolicy::exponential(
228 0,
229 Duration::from_millis(1),
230 Duration::from_millis(1),
231 2,
232 )
233 .unwrap_err(),
234 ModelRetryPolicyError::ZeroMaxAttempts
235 );
236 }
237
238 #[test]
239 fn unknown_error_requires_explicit_retry_authority() {
240 let policy = ModelRetryPolicy::exponential(2, Duration::ZERO, Duration::ZERO, 1).unwrap();
241 let mut error = ModelError::local(ModelErrorKind::Transport, "failure");
242 error.retry_safety = RetrySafety::Unknown;
243 assert!(!policy.permits(&error));
244 assert!(
245 policy
246 .allow_unknown(ModelErrorKind::Transport)
247 .permits(&error)
248 );
249 }
250}