1use std::time::Duration;
25
26use async_trait::async_trait;
27
28use crate::error::{KernelError, Result};
29use crate::llm::client::LLMClient;
30use crate::llm::types::{LLMRequest, LLMResponse, LLMStream};
31
32#[derive(Debug, Clone)]
34pub struct RetryConfig {
35 pub max_retries: u32,
37 pub base_delay: Duration,
39}
40
41impl Default for RetryConfig {
42 fn default() -> Self {
43 Self {
44 max_retries: 3,
45 base_delay: Duration::from_secs(1),
46 }
47 }
48}
49
50pub struct RetryClient<C> {
59 inner: C,
60 config: RetryConfig,
61}
62
63impl<C> RetryClient<C> {
64 pub fn new(inner: C, config: RetryConfig) -> Self {
66 Self { inner, config }
67 }
68
69 pub fn inner(&self) -> &C {
71 &self.inner
72 }
73}
74
75const MAX_RETRY_AFTER_SECS: u64 = 300;
79
80fn retry_delay(err: &KernelError, attempt: u32, base_delay: Duration) -> Option<Duration> {
84 match err {
85 KernelError::RateLimited(secs) => {
86 let server_delay = Duration::from_secs((*secs).min(MAX_RETRY_AFTER_SECS));
89 Some(std::cmp::max(
90 server_delay,
91 backoff_with_jitter(attempt, base_delay),
92 ))
93 }
94 KernelError::Http { status, .. } if (*status >= 500 && *status < 600) => {
95 Some(backoff_with_jitter(attempt, base_delay))
96 }
97 _ => None,
98 }
99}
100
101fn backoff_with_jitter(attempt: u32, base_delay: Duration) -> Duration {
108 let base_ms = base_delay.as_millis() as u64;
109 let exp_ms = base_ms.saturating_mul(1u64 << attempt.min(6));
111 let capped_ms = exp_ms.min(60_000);
112 let nanos = std::time::SystemTime::now()
115 .duration_since(std::time::UNIX_EPOCH)
116 .map(|d| d.subsec_nanos() as u64)
117 .unwrap_or(0);
118 let seed = (attempt as u64)
119 .wrapping_mul(2654435769)
120 .wrapping_add(nanos.wrapping_mul(0x9E3779B97F4A7C15));
121 let jitter_hash = seed % 1000;
122 let jittered_ms = (capped_ms * (500 + jitter_hash) / 1000).min(60_000);
124 Duration::from_millis(jittered_ms)
125}
126
127#[async_trait]
128impl<C: LLMClient> LLMClient for RetryClient<C> {
129 async fn complete(&self, request: LLMRequest) -> Result<LLMResponse> {
130 let mut last_err = None;
131
132 for attempt in 0..=self.config.max_retries {
133 match self.inner.complete(request.clone()).await {
134 Ok(response) => return Ok(response),
135 Err(err) => {
136 if attempt >= self.config.max_retries {
138 return Err(err);
139 }
140
141 let Some(delay) = retry_delay(&err, attempt, self.config.base_delay) else {
143 return Err(err);
144 };
145
146 last_err = Some(err);
147 tokio::time::sleep(delay).await;
148 }
149 }
150 }
151
152 Err(last_err.unwrap())
154 }
155
156 fn model_name(&self) -> &str {
157 self.inner.model_name()
158 }
159
160 async fn stream_complete(&self, request: LLMRequest) -> Result<LLMStream> {
162 self.inner.stream_complete(request).await
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use std::sync::atomic::AtomicU32;
170
171 #[test]
172 fn backoff_increases_with_attempts() {
173 let base = Duration::from_secs(1);
174 let d0 = backoff_with_jitter(0, base);
175 let d1 = backoff_with_jitter(1, base);
176 let d2 = backoff_with_jitter(2, base);
177
178 assert!(d0.as_millis() > 0);
180 assert!(d1.as_millis() > d0.as_millis() / 2);
181 assert!(d2.as_millis() > d1.as_millis() / 2);
182 }
183
184 #[test]
185 fn backoff_capped_at_60s() {
186 let base = Duration::from_secs(10);
187 let d = backoff_with_jitter(20, base);
188 assert!(d <= Duration::from_secs(60));
189 }
190
191 #[test]
192 fn retry_delay_rate_limited_uses_server_hint() {
193 let err = KernelError::RateLimited(30);
194 let delay = retry_delay(&err, 0, Duration::from_secs(1));
195 assert!(delay.is_some());
196 assert!(delay.unwrap() >= Duration::from_secs(30));
198 }
199
200 #[test]
201 fn retry_delay_rate_limited_is_clamped() {
202 let err = KernelError::RateLimited(999_999);
204 let delay = retry_delay(&err, 0, Duration::from_secs(1)).unwrap();
205 assert!(delay <= Duration::from_secs(MAX_RETRY_AFTER_SECS));
206 }
207
208 #[test]
209 fn retry_delay_server_error_returns_backoff() {
210 let err = KernelError::Http {
211 status: 500,
212 message: "error".into(),
213 };
214 let delay = retry_delay(&err, 0, Duration::from_secs(1));
215 assert!(delay.is_some());
216 }
217
218 #[test]
219 fn retry_delay_client_error_returns_none() {
220 let err = KernelError::Http {
221 status: 400,
222 message: "bad request".into(),
223 };
224 let delay = retry_delay(&err, 0, Duration::from_secs(1));
225 assert!(delay.is_none());
226 }
227
228 #[test]
229 fn retry_delay_config_error_returns_none() {
230 let err = KernelError::Config("missing key".into());
231 let delay = retry_delay(&err, 0, Duration::from_secs(1));
232 assert!(delay.is_none());
233 }
234
235 struct MockClient {
237 fail_count: AtomicU32,
238 responses: std::sync::Mutex<Vec<Result<LLMResponse>>>,
239 }
240
241 impl MockClient {
242 fn new(responses: Vec<Result<LLMResponse>>) -> Self {
243 Self {
244 fail_count: AtomicU32::new(0),
245 responses: std::sync::Mutex::new(responses),
246 }
247 }
248 }
249
250 #[async_trait]
251 impl LLMClient for MockClient {
252 async fn complete(&self, _request: LLMRequest) -> Result<LLMResponse> {
253 let mut responses = self.responses.lock().unwrap();
254 if responses.is_empty() {
255 panic!("MockClient: no more responses");
256 }
257 let result = responses.remove(0);
258 if result.is_err() {
259 self.fail_count
260 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
261 }
262 result
263 }
264
265 fn model_name(&self) -> &str {
266 "mock"
267 }
268
269 async fn stream_complete(&self, _request: LLMRequest) -> Result<LLMStream> {
270 unimplemented!()
271 }
272 }
273
274 fn ok_response() -> Result<LLMResponse> {
275 Ok(LLMResponse {
276 content: "ok".into(),
277 model: "mock".into(),
278 ..Default::default()
279 })
280 }
281
282 fn server_error() -> Result<LLMResponse> {
283 Err(KernelError::Http {
284 status: 500,
285 message: "error".into(),
286 })
287 }
288
289 fn rate_limited() -> Result<LLMResponse> {
290 Err(KernelError::RateLimited(0))
291 }
292
293 fn client_error() -> Result<LLMResponse> {
294 Err(KernelError::Http {
295 status: 400,
296 message: "bad request".into(),
297 })
298 }
299
300 #[tokio::test]
301 async fn retry_succeeds_after_transient_failures() {
302 let mock = MockClient::new(vec![server_error(), server_error(), ok_response()]);
303 let retry = RetryClient::new(
304 mock,
305 RetryConfig {
306 max_retries: 3,
307 base_delay: Duration::from_millis(1),
308 },
309 );
310
311 let result = retry.complete(LLMRequest::builder().build()).await;
312 assert!(result.is_ok());
313 assert_eq!(result.unwrap().content, "ok");
314 }
315
316 #[tokio::test]
317 async fn retry_exhausted_returns_last_error() {
318 let mock = MockClient::new(vec![
319 server_error(),
320 server_error(),
321 server_error(),
322 server_error(), ]);
324 let retry = RetryClient::new(
325 mock,
326 RetryConfig {
327 max_retries: 3,
328 base_delay: Duration::from_millis(1),
329 },
330 );
331
332 let result = retry.complete(LLMRequest::builder().build()).await;
333 assert!(result.is_err());
334 }
335
336 #[tokio::test]
337 async fn retry_does_not_retry_client_error() {
338 let mock = MockClient::new(vec![client_error()]);
339 let retry = RetryClient::new(
340 mock,
341 RetryConfig {
342 max_retries: 3,
343 base_delay: Duration::from_millis(1),
344 },
345 );
346
347 let result = retry.complete(LLMRequest::builder().build()).await;
348 assert!(result.is_err());
349 let err = result.unwrap_err();
350 assert!(matches!(err, KernelError::Http { status: 400, .. }));
351 }
352
353 #[tokio::test]
354 async fn retry_handles_rate_limit_with_backoff() {
355 let mock = MockClient::new(vec![rate_limited(), ok_response()]);
356 let retry = RetryClient::new(
357 mock,
358 RetryConfig {
359 max_retries: 3,
360 base_delay: Duration::from_millis(1),
361 },
362 );
363
364 let result = retry.complete(LLMRequest::builder().build()).await;
365 assert!(result.is_ok());
366 }
367
368 #[tokio::test]
369 async fn retry_zero_retries_returns_immediately() {
370 let mock = MockClient::new(vec![server_error()]);
371 let retry = RetryClient::new(
372 mock,
373 RetryConfig {
374 max_retries: 0,
375 base_delay: Duration::from_millis(1),
376 },
377 );
378
379 let result = retry.complete(LLMRequest::builder().build()).await;
380 assert!(result.is_err());
381 }
382
383 #[test]
384 fn default_config() {
385 let config = RetryConfig::default();
386 assert_eq!(config.max_retries, 3);
387 assert_eq!(config.base_delay, Duration::from_secs(1));
388 }
389}