1use std::fmt;
19use std::future::Future;
20use std::pin::Pin;
21use std::sync::Arc;
22use std::task::{Context, Poll};
23use std::time::Duration;
24
25use bytes::{Bytes, BytesMut};
26use http::header::RETRY_AFTER;
27use http::{HeaderMap, StatusCode};
28use http_body_util::{BodyDataStream, BodyExt};
29use tower::retry::Policy;
30use tower::{Layer, Service};
31
32use crate::{Body, BoxError};
33
34const RETRY_AFTER_MS: &str = "retry-after-ms";
36
37pub const DEFAULT_BODY_LIMIT: usize = 64 * 1024;
39
40#[derive(Clone, Debug)]
51pub struct RetryPolicy {
52 max_retries: usize,
53 attempts: usize,
54 base_delay: Duration,
55 max_delay: Duration,
56 max_retry_after: Duration,
57 jitter: bool,
58}
59
60impl RetryPolicy {
61 pub fn new(max_retries: usize) -> Self {
63 Self {
64 max_retries,
65 attempts: 0,
66 base_delay: Duration::from_millis(200),
67 max_delay: Duration::from_secs(8),
68 max_retry_after: Duration::from_secs(60),
69 jitter: true,
70 }
71 }
72
73 pub fn base_delay(mut self, delay: Duration) -> Self {
75 self.base_delay = delay;
76 self
77 }
78
79 pub fn max_delay(mut self, delay: Duration) -> Self {
82 self.max_delay = delay;
83 self
84 }
85
86 pub fn max_retry_after(mut self, delay: Duration) -> Self {
89 self.max_retry_after = delay;
90 self
91 }
92
93 pub fn jitter(mut self, jitter: bool) -> Self {
96 self.jitter = jitter;
97 self
98 }
99
100 pub fn max_retries(&self) -> usize {
102 self.max_retries
103 }
104
105 fn delay(&self, retry_after: Option<Duration>) -> Option<Duration> {
107 if let Some(retry_after) = retry_after {
108 return (retry_after <= self.max_retry_after).then_some(retry_after);
109 }
110 let backoff = self
111 .base_delay
112 .saturating_mul(1u32 << self.attempts.min(16))
113 .min(self.max_delay);
114 Some(if self.jitter {
115 jittered(backoff, random_unit())
116 } else {
117 backoff
118 })
119 }
120}
121
122impl Default for RetryPolicy {
124 fn default() -> Self {
125 Self::new(3)
126 }
127}
128
129fn jittered(backoff: Duration, unit: f64) -> Duration {
131 backoff.mul_f64(0.5 + 0.5 * unit)
132}
133
134fn random_unit() -> f64 {
136 use std::hash::{BuildHasher, Hasher};
137 let hash = std::collections::hash_map::RandomState::new()
138 .build_hasher()
139 .finish();
140 (hash >> 11) as f64 / (1u64 << 53) as f64
141}
142
143fn retryable<B>(result: &Result<http::Response<B>, BoxError>) -> Option<Option<Duration>> {
145 match result {
146 Ok(response) => {
147 let status = response.status();
148 let transient = status == StatusCode::TOO_MANY_REQUESTS
149 || status == StatusCode::REQUEST_TIMEOUT
150 || status.is_server_error();
151 let permanent = response.extensions().get::<Permanent>().is_some();
152 (transient && !permanent).then(|| retry_after(response.headers()))
153 }
154 Err(error) => is_connect(error.as_ref()).then_some(None),
155 }
156}
157
158fn retry_after(headers: &HeaderMap) -> Option<Duration> {
161 let parse = |name, scale: f64| {
162 let value = headers
163 .get(name)?
164 .to_str()
165 .ok()?
166 .trim()
167 .parse::<f64>()
168 .ok()?;
169 Duration::try_from_secs_f64(value * scale).ok()
170 };
171 parse(RETRY_AFTER_MS, 0.001).or_else(|| parse(RETRY_AFTER.as_str(), 1.0))
172}
173
174#[allow(unused_variables)]
175fn is_connect(error: &(dyn std::error::Error + 'static)) -> bool {
176 #[cfg(feature = "reqwest")]
177 {
178 let mut source = Some(error);
179 while let Some(error) = source {
180 if error
181 .downcast_ref::<reqwest::Error>()
182 .is_some_and(reqwest::Error::is_connect)
183 {
184 return true;
185 }
186 source = error.source();
187 }
188 }
189 false
190}
191
192impl<B> Policy<http::Request<Body>, http::Response<B>, BoxError> for RetryPolicy {
193 type Future = Pin<Box<dyn Future<Output = ()> + Send>>;
194
195 fn retry(
196 &mut self,
197 _request: &mut http::Request<Body>,
198 result: &mut Result<http::Response<B>, BoxError>,
199 ) -> Option<Self::Future> {
200 if self.attempts >= self.max_retries {
201 return None;
202 }
203 let retry_after = retryable(result)?;
204 let Some(delay) = self.delay(retry_after) else {
205 #[cfg(feature = "tracing")]
206 tracing::debug!(
207 target: "timvw_rust_runtime::trace",
208 retry_after_ms = retry_after.map_or(0, |delay| delay.as_millis() as u64),
209 max_retry_after_ms = self.max_retry_after.as_millis() as u64,
210 status = result.as_ref().ok().map(|response| response.status().as_u16()),
211 "not retrying: the server asks to wait longer than max_retry_after"
212 );
213 return None;
214 };
215 self.attempts += 1;
216 #[cfg(feature = "tracing")]
217 tracing::debug!(
218 target: "timvw_rust_runtime::trace",
219 attempt = self.attempts,
220 max_retries = self.max_retries,
221 delay_ms = delay.as_millis() as u64,
222 status = result.as_ref().ok().map(|response| response.status().as_u16()),
223 "retrying"
224 );
225 Some(Box::pin(tokio::time::sleep(delay)))
226 }
227
228 fn clone_request(&mut self, request: &http::Request<Body>) -> Option<http::Request<Body>> {
229 let body = request.body().try_clone()?;
230 let mut clone = http::Request::new(body);
231 *clone.method_mut() = request.method().clone();
232 *clone.uri_mut() = request.uri().clone();
233 *clone.version_mut() = request.version();
234 *clone.headers_mut() = request.headers().clone();
235 Some(clone)
236 }
237}
238
239#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
244pub struct Permanent;
245
246type Classifier = Arc<dyn Fn(&[u8]) -> bool + Send + Sync>;
248
249pub fn openai_insufficient_quota(body: &[u8]) -> bool {
254 const INSUFFICIENT_QUOTA: &str = "insufficient_quota";
255 let Ok(value) = serde_json::from_slice::<serde_json::Value>(body) else {
256 return false;
257 };
258 ["code", "type"].iter().any(|field| {
259 value
260 .pointer(&format!("/error/{field}"))
261 .and_then(serde_json::Value::as_str)
262 == Some(INSUFFICIENT_QUOTA)
263 })
264}
265
266#[derive(Clone)]
285pub struct RetryLayer {
286 policy: RetryPolicy,
287 classify: Option<Classifier>,
288 body_limit: usize,
289}
290
291impl RetryLayer {
292 pub fn new(policy: RetryPolicy) -> Self {
294 Self {
295 policy,
296 classify: None,
297 body_limit: DEFAULT_BODY_LIMIT,
298 }
299 }
300
301 pub fn permanent_429<F>(mut self, is_permanent: F) -> Self
306 where
307 F: Fn(&[u8]) -> bool + Send + Sync + 'static,
308 {
309 self.classify = Some(Arc::new(is_permanent));
310 self
311 }
312
313 pub fn body_limit(mut self, limit: usize) -> Self {
315 self.body_limit = limit;
316 self
317 }
318
319 pub fn policy(&self) -> &RetryPolicy {
321 &self.policy
322 }
323}
324
325impl Default for RetryLayer {
327 fn default() -> Self {
328 Self::new(RetryPolicy::default())
329 }
330}
331
332impl fmt::Debug for RetryLayer {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 f.debug_struct("RetryLayer")
335 .field("policy", &self.policy)
336 .field("permanent_429", &self.classify.is_some())
337 .field("body_limit", &self.body_limit)
338 .finish()
339 }
340}
341
342impl<S> Layer<S> for RetryLayer {
343 type Service = tower::retry::Retry<RetryPolicy, Classify429<S>>;
344
345 fn layer(&self, inner: S) -> Self::Service {
346 tower::retry::Retry::new(
347 self.policy.clone(),
348 Classify429 {
349 inner,
350 classify: self.classify.clone(),
351 body_limit: self.body_limit,
352 },
353 )
354 }
355}
356
357#[derive(Clone)]
359pub struct Classify429<S> {
360 inner: S,
361 classify: Option<Classifier>,
362 body_limit: usize,
363}
364
365impl<S: fmt::Debug> fmt::Debug for Classify429<S> {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 f.debug_struct("Classify429")
368 .field("inner", &self.inner)
369 .field("permanent_429", &self.classify.is_some())
370 .field("body_limit", &self.body_limit)
371 .finish()
372 }
373}
374
375impl<S, R, B> Service<R> for Classify429<S>
376where
377 S: Service<R, Response = http::Response<B>>,
378 S::Future: Send + 'static,
379 B: http_body::Body + Send + Sync + 'static,
380 B::Data: Into<Bytes>,
381 B::Error: Into<BoxError>,
382{
383 type Response = http::Response<Body>;
384 type Error = S::Error;
385 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
386
387 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
388 self.inner.poll_ready(cx)
389 }
390
391 fn call(&mut self, request: R) -> Self::Future {
392 let response = self.inner.call(request);
393 let classify = self.classify.clone();
394 let body_limit = self.body_limit;
395 Box::pin(async move {
396 let response = response.await?.map(Body::wrap);
397 Ok(match classify {
398 Some(classify) if response.status() == StatusCode::TOO_MANY_REQUESTS => {
399 classify_429(response, &*classify, body_limit).await
400 }
401 _ => response,
402 })
403 })
404 }
405}
406
407async fn classify_429(
410 response: http::Response<Body>,
411 classify: &(dyn Fn(&[u8]) -> bool + Send + Sync),
412 limit: usize,
413) -> http::Response<Body> {
414 let (mut parts, mut body) = response.into_parts();
415 let mut read = BytesMut::new();
416 let body = loop {
417 match body.frame().await {
418 None => {
419 let read = read.freeze();
420 if classify(&read) {
421 parts.extensions.insert(Permanent);
422 #[cfg(feature = "tracing")]
423 tracing::debug!(
424 target: "timvw_rust_runtime::trace",
425 status = parts.status.as_u16(),
426 "not retrying: the body says the error is permanent"
427 );
428 }
429 break Body::from(read);
430 }
431 Some(Ok(frame)) => {
433 let Ok(data) = frame.into_data() else {
434 continue;
435 };
436 read.extend_from_slice(&data);
437 if read.len() > limit {
438 let read = futures_util::stream::once(async move { Ok(read.freeze()) });
439 break Body::wrap_stream(futures_util::StreamExt::chain(
440 read,
441 BodyDataStream::new(body),
442 ));
443 }
444 }
445 Some(Err(error)) => {
446 break Body::wrap_stream(futures_util::stream::iter([
447 Ok(read.freeze()),
448 Err::<Bytes, BoxError>(error),
449 ]));
450 }
451 }
452 };
453 http::Response::from_parts(parts, body)
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 fn response(status: u16, headers: &[(&str, &str)]) -> Result<http::Response<()>, BoxError> {
461 let mut builder = http::Response::builder().status(status);
462 for (name, value) in headers {
463 builder = builder.header(*name, *value);
464 }
465 Ok(builder.body(()).unwrap())
466 }
467
468 #[test]
469 fn retries_transient_statuses_only() {
470 assert_eq!(retryable(&response(429, &[])), Some(None));
471 assert_eq!(
472 retryable(&response(503, &[("retry-after", "2")])),
473 Some(Some(Duration::from_secs(2)))
474 );
475 assert_eq!(retryable(&response(408, &[])), Some(None));
476 assert_eq!(retryable(&response(400, &[])), None);
477 assert_eq!(retryable(&response(200, &[])), None);
478 assert_eq!(retryable::<()>(&Err("boom".into())), None);
479 }
480
481 #[test]
482 fn permanent_responses_are_not_retried() {
483 let mut result = response(429, &[]);
484 result.as_mut().unwrap().extensions_mut().insert(Permanent);
485 assert_eq!(retryable(&result), None);
486 }
487
488 #[test]
489 fn parses_retry_after_and_retry_after_ms() {
490 let wait = |headers: &[(&str, &str)]| retryable(&response(429, headers)).unwrap();
491 assert_eq!(
492 wait(&[("retry-after", " 20 ")]),
493 Some(Duration::from_secs(20))
494 );
495 assert_eq!(
496 wait(&[("retry-after", "1.5")]),
497 Some(Duration::from_millis(1500))
498 );
499 assert_eq!(
500 wait(&[("retry-after-ms", "250"), ("retry-after", "1")]),
501 Some(Duration::from_millis(250))
502 );
503 assert_eq!(
504 wait(&[("retry-after-ms", "12.5")]),
505 Some(Duration::from_micros(12_500))
506 );
507 assert_eq!(
508 wait(&[("retry-after", "Wed, 21 Oct 2015 07:28:00 GMT")]),
509 None
510 );
511 assert_eq!(wait(&[("retry-after", "-1")]), None);
512 assert_eq!(
513 wait(&[("retry-after-ms", "nan"), ("retry-after", "3")]),
514 Some(Duration::from_secs(3))
515 );
516 }
517
518 #[test]
519 fn backs_off_and_caps() {
520 let mut policy = RetryPolicy::new(10)
521 .base_delay(Duration::from_millis(100))
522 .max_delay(Duration::from_secs(1))
523 .jitter(false);
524 assert_eq!(policy.delay(None), Some(Duration::from_millis(100)));
525 policy.attempts = 2;
526 assert_eq!(policy.delay(None), Some(Duration::from_millis(400)));
527 policy.attempts = 8;
528 assert_eq!(policy.delay(None), Some(Duration::from_secs(1)));
529 }
530
531 #[test]
532 fn honours_retry_after_beyond_max_delay_up_to_max_retry_after() {
533 let policy = RetryPolicy::new(3).max_delay(Duration::from_secs(1));
534 assert_eq!(
535 policy.delay(Some(Duration::from_secs(30))),
536 Some(Duration::from_secs(30))
537 );
538 assert_eq!(
539 policy.delay(Some(Duration::from_secs(60))),
540 Some(Duration::from_secs(60))
541 );
542 assert_eq!(policy.delay(Some(Duration::from_secs(61))), None);
543 let policy = policy.max_retry_after(Duration::from_secs(5));
544 assert_eq!(policy.delay(Some(Duration::from_secs(6))), None);
545 }
546
547 #[test]
548 fn jitter_stays_between_half_and_all_of_the_backoff() {
549 let backoff = Duration::from_millis(800);
550 assert_eq!(jittered(backoff, 0.0), Duration::from_millis(400));
551 assert_eq!(jittered(backoff, 0.5), Duration::from_millis(600));
552 assert!(jittered(backoff, 0.999_999) <= backoff);
553
554 let mut policy = RetryPolicy::new(10)
555 .base_delay(Duration::from_millis(100))
556 .max_delay(Duration::from_secs(1));
557 policy.attempts = 3; let delays: Vec<_> = (0..200).map(|_| policy.delay(None).unwrap()).collect();
559 for delay in &delays {
560 assert!(
561 (Duration::from_millis(400)..=backoff).contains(delay),
562 "{delay:?}"
563 );
564 }
565 assert!(delays.iter().any(|delay| *delay != delays[0]), "random");
566 assert_eq!(
568 policy.delay(Some(Duration::from_secs(2))),
569 Some(Duration::from_secs(2))
570 );
571 }
572
573 #[test]
574 fn random_unit_is_in_range() {
575 for _ in 0..1000 {
576 assert!((0.0..1.0).contains(&random_unit()));
577 }
578 }
579
580 #[test]
581 fn recognises_openai_insufficient_quota() {
582 assert!(openai_insufficient_quota(
583 br#"{"error":{"message":"You exceeded your current quota","type":"insufficient_quota","param":null,"code":"insufficient_quota"}}"#
584 ));
585 assert!(openai_insufficient_quota(
586 br#"{"error":{"type":"insufficient_quota"}}"#
587 ));
588 assert!(openai_insufficient_quota(
589 br#"{"error":{"type":"requests","code":"insufficient_quota"}}"#
590 ));
591 assert!(!openai_insufficient_quota(
592 br#"{"error":{"message":"Rate limit reached","type":"requests","code":"rate_limit_exceeded"}}"#
593 ));
594 assert!(!openai_insufficient_quota(b"insufficient_quota"));
595 assert!(!openai_insufficient_quota(b""));
596 }
597
598 #[test]
599 fn only_bodies_in_memory_are_cloned() {
600 let mut policy = RetryPolicy::default();
601 let mut clone_request = |request: &http::Request<Body>| {
602 Policy::<_, http::Response<()>, BoxError>::clone_request(&mut policy, request)
603 };
604 let request = http::Request::post("https://example.com")
605 .header("x", "1")
606 .body(Body::from("a"))
607 .unwrap();
608 let clone = clone_request(&request).unwrap();
609 assert_eq!(clone.uri(), request.uri());
610 assert_eq!(clone.headers()["x"], "1");
611 let chunks =
612 futures_util::stream::iter(vec![Ok::<_, std::io::Error>(bytes::Bytes::from("a"))]);
613 let streamed = http::Request::post("https://example.com")
614 .body(Body::wrap_stream(chunks))
615 .unwrap();
616 assert!(clone_request(&streamed).is_none());
617 }
618
619 fn body_429(chunks: Vec<Result<&'static str, &'static str>>) -> http::Response<Body> {
620 let stream = futures_util::stream::iter(
621 chunks
622 .into_iter()
623 .map(|chunk| chunk.map(Bytes::from).map_err(BoxError::from)),
624 );
625 http::Response::builder()
626 .status(429)
627 .body(Body::wrap_stream(stream))
628 .unwrap()
629 }
630
631 #[tokio::test]
632 async fn classifies_the_whole_body_and_puts_it_back() {
633 let response = body_429(vec![
634 Ok(r#"{"error":{"type":"#),
635 Ok(r#""insufficient_quota"}}"#),
636 ]);
637 let response = classify_429(response, &openai_insufficient_quota, 1024).await;
638 assert!(response.extensions().get::<Permanent>().is_some());
639 let body = response.into_body().collect_bytes().await.unwrap();
640 assert_eq!(body, r#"{"error":{"type":"insufficient_quota"}}"#);
641 }
642
643 #[tokio::test]
644 async fn a_body_over_the_limit_is_not_classified_but_kept_whole() {
645 let response = body_429(vec![Ok("0123"), Ok("4567"), Ok("89")]);
646 let response = classify_429(response, &|_: &[u8]| true, 5).await;
647 assert!(response.extensions().get::<Permanent>().is_none());
648 let body = response.into_body().collect_bytes().await.unwrap();
649 assert_eq!(body, "0123456789");
650 }
651
652 #[tokio::test]
653 async fn a_body_that_fails_is_not_classified_and_keeps_its_error() {
654 let response = body_429(vec![Ok("01"), Err("reset")]);
655 let response = classify_429(response, &|_: &[u8]| true, 1024).await;
656 assert!(response.extensions().get::<Permanent>().is_none());
657 let error = response.into_body().collect_bytes().await.unwrap_err();
658 assert_eq!(error.to_string(), "reset");
659 }
660}