timvw_rust_runtime/
retry.rs1use std::future::Future;
16use std::pin::Pin;
17use std::time::Duration;
18
19use http::header::RETRY_AFTER;
20use http::StatusCode;
21use tower::retry::Policy;
22
23use crate::{Body, BoxError};
24
25#[derive(Clone, Debug)]
31pub struct RetryPolicy {
32 max_retries: usize,
33 attempts: usize,
34 base_delay: Duration,
35 max_delay: Duration,
36}
37
38impl RetryPolicy {
39 pub fn new(max_retries: usize) -> Self {
41 Self {
42 max_retries,
43 attempts: 0,
44 base_delay: Duration::from_millis(200),
45 max_delay: Duration::from_secs(8),
46 }
47 }
48
49 pub fn base_delay(mut self, delay: Duration) -> Self {
51 self.base_delay = delay;
52 self
53 }
54
55 pub fn max_delay(mut self, delay: Duration) -> Self {
57 self.max_delay = delay;
58 self
59 }
60
61 pub fn max_retries(&self) -> usize {
63 self.max_retries
64 }
65
66 fn delay(&self, retry_after: Option<Duration>) -> Duration {
67 let backoff = self
68 .base_delay
69 .saturating_mul(1u32 << self.attempts.min(16));
70 retry_after.unwrap_or(backoff).min(self.max_delay)
71 }
72}
73
74impl Default for RetryPolicy {
76 fn default() -> Self {
77 Self::new(3)
78 }
79}
80
81fn retryable<B>(result: &Result<http::Response<B>, BoxError>) -> Option<Option<Duration>> {
83 match result {
84 Ok(response) => {
85 let status = response.status();
86 let transient = status == StatusCode::TOO_MANY_REQUESTS
87 || status == StatusCode::REQUEST_TIMEOUT
88 || status.is_server_error();
89 transient.then(|| {
90 response
91 .headers()
92 .get(RETRY_AFTER)
93 .and_then(|value| value.to_str().ok())
94 .and_then(|value| value.trim().parse::<u64>().ok())
95 .map(Duration::from_secs)
96 })
97 }
98 Err(error) => is_connect(error.as_ref()).then_some(None),
99 }
100}
101
102#[allow(unused_variables)]
103fn is_connect(error: &(dyn std::error::Error + 'static)) -> bool {
104 #[cfg(feature = "reqwest")]
105 {
106 let mut source = Some(error);
107 while let Some(error) = source {
108 if error
109 .downcast_ref::<reqwest::Error>()
110 .is_some_and(reqwest::Error::is_connect)
111 {
112 return true;
113 }
114 source = error.source();
115 }
116 }
117 false
118}
119
120impl<B> Policy<http::Request<Body>, http::Response<B>, BoxError> for RetryPolicy {
121 type Future = Pin<Box<dyn Future<Output = ()> + Send>>;
122
123 fn retry(
124 &mut self,
125 _request: &mut http::Request<Body>,
126 result: &mut Result<http::Response<B>, BoxError>,
127 ) -> Option<Self::Future> {
128 if self.attempts >= self.max_retries {
129 return None;
130 }
131 let retry_after = retryable(result)?;
132 let delay = self.delay(retry_after);
133 self.attempts += 1;
134 #[cfg(feature = "tracing")]
135 tracing::debug!(
136 target: "timvw_rust_runtime::trace",
137 attempt = self.attempts,
138 max_retries = self.max_retries,
139 delay_ms = delay.as_millis() as u64,
140 status = result.as_ref().ok().map(|response| response.status().as_u16()),
141 "retrying"
142 );
143 Some(Box::pin(tokio::time::sleep(delay)))
144 }
145
146 fn clone_request(&mut self, request: &http::Request<Body>) -> Option<http::Request<Body>> {
147 let body = request.body().try_clone()?;
148 let mut clone = http::Request::new(body);
149 *clone.method_mut() = request.method().clone();
150 *clone.uri_mut() = request.uri().clone();
151 *clone.version_mut() = request.version();
152 *clone.headers_mut() = request.headers().clone();
153 Some(clone)
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 fn response(status: u16, retry_after: Option<&str>) -> Result<http::Response<()>, BoxError> {
162 let mut builder = http::Response::builder().status(status);
163 if let Some(value) = retry_after {
164 builder = builder.header(RETRY_AFTER, value);
165 }
166 Ok(builder.body(()).unwrap())
167 }
168
169 #[test]
170 fn retries_transient_statuses_only() {
171 assert_eq!(retryable(&response(429, None)), Some(None));
172 assert_eq!(
173 retryable(&response(503, Some("2"))),
174 Some(Some(Duration::from_secs(2)))
175 );
176 assert_eq!(retryable(&response(408, None)), Some(None));
177 assert_eq!(retryable(&response(400, None)), None);
178 assert_eq!(retryable(&response(200, None)), None);
179 assert_eq!(retryable::<()>(&Err("boom".into())), None);
180 }
181
182 #[test]
183 fn backs_off_and_caps() {
184 let mut policy = RetryPolicy::new(10)
185 .base_delay(Duration::from_millis(100))
186 .max_delay(Duration::from_secs(1));
187 assert_eq!(policy.delay(None), Duration::from_millis(100));
188 policy.attempts = 2;
189 assert_eq!(policy.delay(None), Duration::from_millis(400));
190 policy.attempts = 8;
191 assert_eq!(policy.delay(None), Duration::from_secs(1));
192 assert_eq!(
193 policy.delay(Some(Duration::from_secs(30))),
194 Duration::from_secs(1)
195 );
196 }
197
198 #[test]
199 fn only_bodies_in_memory_are_cloned() {
200 let mut policy = RetryPolicy::default();
201 let mut clone_request = |request: &http::Request<Body>| {
202 Policy::<_, http::Response<()>, BoxError>::clone_request(&mut policy, request)
203 };
204 let request = http::Request::post("https://example.com")
205 .header("x", "1")
206 .body(Body::from("a"))
207 .unwrap();
208 let clone = clone_request(&request).unwrap();
209 assert_eq!(clone.uri(), request.uri());
210 assert_eq!(clone.headers()["x"], "1");
211 let chunks =
212 futures_util::stream::iter(vec![Ok::<_, std::io::Error>(bytes::Bytes::from("a"))]);
213 let streamed = http::Request::post("https://example.com")
214 .body(Body::wrap_stream(chunks))
215 .unwrap();
216 assert!(clone_request(&streamed).is_none());
217 }
218}