Skip to main content

rama_http/layer/retry/
mod.rs

1//! Middleware for retrying "failed" requests.
2//!
3//! Every attempt runs on its own [`fork`](Request::fork_extensions_in_place) of the caller's
4//! request [`Extensions`](rama_core::extensions::Extensions): an attempt reads everything the
5//! caller inserted, while what it (or any inner layer) inserts stays isolated from the caller and
6//! from every other attempt. A failed attempt therefore cannot steer the next one by inserting.
7//!
8//! Isolation is structural, not deep: an inherited value is shared by handle, so interior mutation
9//! through it stays visible to the caller and to every other attempt — which is exactly how a
10//! caller-owned counter or budget is meant to work. Only the entries an attempt inserts itself are
11//! private to it.
12//!
13//! This holds whether or not the request turns out to be retryable: what the [`Policy`] decides
14//! about retrying has no bearing on who owns the request's extensions.
15//!
16//! The [`Policy`] itself is the one exception to that isolation: it is handed a request backed by
17//! the caller's own store, so metadata it records there (a retry count, an exhaustion marker)
18//! reaches the next attempt and stays visible to the caller. Only the inner service's inserts are
19//! private to an attempt.
20//!
21//! # Layer placement
22//!
23//! Place [`RetryLayer`] as early — as far outward — in the stack as your use case allows, in front
24//! of everything whose work depends on the request: route or proxy selection, DNS overwrites,
25//! per-target limits. Such a layer placed _outside_ runs exactly once, and every attempt then
26//! reuses its verdict — retrying through the very proxy or address that just failed. Placed
27//! _inside_, it is consulted per attempt.
28//!
29//! One counterweight: retrying requires a replayable body, so this middleware buffers the request
30//! body in full ([`RetryBody`]) and every layer inside it sees that buffered body instead of the
31//! original stream.
32//!
33//! [`follow_redirect`](crate::layer::follow_redirect) follows the same rules for redirect hops. Keep
34//! that middleware outside this one, so a retry replays one hop instead of the entire chain.
35
36use crate::{Request, StreamingBody, body::util::BodyExt};
37use rama_core::Service;
38use rama_core::error::BoxError;
39use rama_utils::macros::define_inner_service_accessors;
40
41mod layer;
42mod policy;
43
44mod body;
45#[doc(inline)]
46pub use body::RetryBody;
47
48pub mod managed;
49pub use managed::ManagedPolicy;
50
51#[cfg(test)]
52mod tests;
53
54pub use self::layer::RetryLayer;
55pub use self::policy::{Policy, PolicyResult};
56
57/// Configure retrying requests of "failed" responses.
58///
59/// A [`Policy`] classifies what is a "failed" response.
60#[derive(Debug, Clone)]
61pub struct Retry<P, S> {
62    policy: P,
63    inner: S,
64}
65
66// ===== impl Retry =====
67
68impl<P, S> Retry<P, S> {
69    /// Retry the inner service depending on this [`Policy`].
70    pub const fn new(policy: P, service: S) -> Self {
71        Self {
72            policy,
73            inner: service,
74        }
75    }
76
77    define_inner_service_accessors!();
78}
79
80#[derive(Debug)]
81/// Error type for [`Retry`]
82pub struct RetryError {
83    kind: RetryErrorKind,
84    inner: Option<BoxError>,
85}
86
87#[derive(Debug)]
88enum RetryErrorKind {
89    BodyConsume,
90    Service,
91}
92
93impl std::fmt::Display for RetryError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match &self.inner {
96            Some(inner) => write!(f, "{}: {}", self.kind, inner),
97            None => write!(f, "{}", self.kind),
98        }
99    }
100}
101
102impl std::fmt::Display for RetryErrorKind {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        match self {
105            Self::BodyConsume => write!(f, "failed to consume body"),
106            Self::Service => write!(f, "service error"),
107        }
108    }
109}
110
111impl std::error::Error for RetryError {
112    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
113        self.inner.as_ref().and_then(|e| e.source())
114    }
115}
116
117impl<P, S, Body> Service<Request<Body>> for Retry<P, S>
118where
119    P: Policy<S::Output, S::Error>,
120    S: Service<Request<RetryBody>, Error: Into<BoxError>>,
121    Body: StreamingBody<Data: Send + 'static, Error: Into<BoxError>> + Send + 'static,
122{
123    type Output = S::Output;
124    type Error = RetryError;
125
126    async fn serve(&self, request: Request<Body>) -> Result<Self::Output, Self::Error> {
127        // consume body so we can clone the request if desired
128        let (parts, body) = request.into_parts();
129        let body = body.collect().await.map_err(|e| RetryError {
130            kind: RetryErrorKind::BodyConsume,
131            inner: Some(e.into()),
132        })?;
133        let body = RetryBody::new(body.to_bytes());
134        let mut request = Request::from_parts(parts, body);
135
136        let mut cloned = self.policy.clone_input(&request);
137
138        // Fork per attempt, so an attempt's inserts reach neither the next attempt nor the caller —
139        // unconditionally, as whether a request can be retried is no business of who owns its
140        // extensions. Clones are taken before the fork so a retry forks from the caller's store,
141        // not from the failed attempt's.
142        let parent_ext = request.fork_extensions_in_place();
143        loop {
144            let resp = self.inner.serve(request).await;
145            match cloned.take() {
146                Some(cloned_req) => {
147                    let cloned_req = match self.policy.retry(cloned_req, resp).await {
148                        PolicyResult::Abort(result) => {
149                            return result.map_err(|e| RetryError {
150                                kind: RetryErrorKind::Service,
151                                inner: Some(e.into()),
152                            });
153                        }
154                        PolicyResult::Retry { req } => req,
155                    };
156
157                    request = cloned_req;
158                    // A policy may return a freshly built request. Restore the
159                    // caller store before cloning the next policy view, then
160                    // fork the request actually sent to the service.
161                    request.set_extensions(parent_ext.clone());
162                    cloned = self.policy.clone_input(&request);
163                    request.set_extensions(parent_ext.fork());
164                }
165                // no clone was made, so no possibility to retry
166                None => {
167                    return resp.map_err(|e| RetryError {
168                        kind: RetryErrorKind::Service,
169                        inner: Some(e.into()),
170                    });
171                }
172            }
173        }
174    }
175}
176
177#[cfg(test)]
178mod test {
179    use super::*;
180    use crate::{
181        BodyExtractExt, Response, StatusCode, layer::retry::managed::DoNotRetry,
182        service::web::response::IntoResponse,
183    };
184    use rama_core::{
185        Layer,
186        error::BoxErrorExt,
187        extensions::{Extension, Extensions, ExtensionsRef},
188        service::service_fn,
189    };
190    use rama_utils::{backoff::ExponentialBackoff, rng::HasherRng};
191    use std::{sync::atomic::AtomicUsize, time::Duration};
192
193    #[tokio::test]
194    async fn test_service_with_managed_retry() {
195        let backoff = ExponentialBackoff::new(
196            Duration::from_millis(1),
197            Duration::from_millis(5),
198            0.1,
199            HasherRng::default,
200        )
201        .unwrap();
202
203        #[derive(Debug, Extension)]
204        struct State {
205            retry_counter: AtomicUsize,
206        }
207
208        async fn retry<Body, E>(
209            req: Request<Body>,
210            result: Result<Response, E>,
211        ) -> (Request<Body>, Result<Response, E>, bool) {
212            if req.extensions().contains::<DoNotRetry>() {
213                panic!("unexpected retry: should be disabled");
214            }
215
216            if let Ok(ref res) = result {
217                if res.status().is_server_error() {
218                    req.extensions()
219                        .get_ref::<State>()
220                        .unwrap()
221                        .retry_counter
222                        .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
223                    (req, result, true)
224                } else {
225                    (req, result, false)
226                }
227            } else {
228                req.extensions()
229                    .get_ref::<State>()
230                    .unwrap()
231                    .retry_counter
232                    .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
233                (req, result, true)
234            }
235        }
236
237        let retry_policy = ManagedPolicy::new(retry).with_backoff(backoff);
238
239        let service = RetryLayer::new(retry_policy).into_layer(service_fn(
240            async |req: Request<RetryBody>| {
241                let txt = req.try_into_string().await.unwrap();
242                match txt.as_str() {
243                    "internal" => Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
244                    "error" => Err(BoxError::from_static_str("custom error")),
245                    _ => Ok(txt.into_response()),
246                }
247            },
248        ));
249
250        fn request(s: &'static str) -> Request {
251            Request::builder().body(s.into()).unwrap()
252        }
253
254        fn extensions() -> Extensions {
255            let extensions = Extensions::new();
256            extensions.insert(State {
257                retry_counter: AtomicUsize::new(0),
258            });
259            extensions
260        }
261
262        fn do_not_retry_extensions() -> Extensions {
263            let extensions = extensions();
264            extensions.insert(DoNotRetry::default());
265            extensions
266        }
267
268        async fn assert_serve_ok<E: std::fmt::Debug>(
269            msg: &'static str,
270            input: &'static str,
271            output: &'static str,
272            extensions: Extensions,
273            retried: bool,
274            service: &impl Service<Request, Output = Response, Error = E>,
275        ) {
276            let state = extensions.get_arc::<State>().unwrap();
277
278            let request = request(input);
279            request.extensions().extend(&extensions);
280
281            let fut = service.serve(request);
282            let res = fut.await.unwrap();
283
284            let body = res.try_into_string().await.unwrap();
285            assert_eq!(body, output, "{msg}");
286            if retried {
287                assert!(
288                    state
289                        .retry_counter
290                        .load(std::sync::atomic::Ordering::Acquire)
291                        > 0,
292                    "{msg}"
293                );
294            } else {
295                assert_eq!(
296                    state
297                        .retry_counter
298                        .load(std::sync::atomic::Ordering::Acquire),
299                    0,
300                    "{msg}"
301                );
302            }
303        }
304
305        async fn assert_serve_err<E: std::fmt::Debug>(
306            msg: &'static str,
307            input: &'static str,
308            extensions: Extensions,
309            retried: bool,
310            service: &impl Service<Request, Output = Response, Error = E>,
311        ) {
312            let state = extensions.get_arc::<State>().unwrap();
313
314            let request = request(input);
315            request.extensions().extend(&extensions);
316
317            let fut = service.serve(request);
318            let res = fut.await;
319
320            assert!(res.is_err(), "{msg}");
321            if retried {
322                assert!(
323                    state
324                        .retry_counter
325                        .load(std::sync::atomic::Ordering::Acquire)
326                        > 0,
327                    "{msg}"
328                );
329            } else {
330                assert_eq!(
331                    state
332                        .retry_counter
333                        .load(std::sync::atomic::Ordering::Acquire),
334                    0,
335                    "{msg}"
336                )
337            }
338        }
339
340        assert_serve_ok(
341            "ok response should be aborted as response without retry",
342            "hello",
343            "hello",
344            extensions(),
345            false,
346            &service,
347        )
348        .await;
349        assert_serve_ok(
350            "internal will trigger 500 with a retry",
351            "internal",
352            "",
353            extensions(),
354            true,
355            &service,
356        )
357        .await;
358        assert_serve_err(
359            "error will trigger an actual non-http error with a retry",
360            "error",
361            extensions(),
362            true,
363            &service,
364        )
365        .await;
366
367        assert_serve_ok(
368            "normally internal will trigger a 500 with retry, but using DoNotRetry will disable retrying",
369            "internal",
370            "",
371            do_not_retry_extensions(),
372            false,
373            &service,
374        ).await;
375    }
376
377    #[tokio::test]
378    async fn inner_layer_do_not_retry_insert_does_not_stop_retries() {
379        use std::sync::Arc;
380        use std::sync::atomic::{AtomicUsize, Ordering};
381
382        async fn retry_on_server_error<Body, E>(
383            req: Request<Body>,
384            result: Result<Response, E>,
385        ) -> (Request<Body>, Result<Response, E>, bool) {
386            let retry = matches!(&result, Ok(res) if res.status().is_server_error());
387            (req, result, retry)
388        }
389
390        let backoff = ExponentialBackoff::new(
391            Duration::from_millis(1),
392            Duration::from_millis(5),
393            0.1,
394            HasherRng::default,
395        )
396        .unwrap();
397
398        let calls = Arc::new(AtomicUsize::new(0));
399        let inner_calls = calls.clone();
400        let service =
401            RetryLayer::new(ManagedPolicy::new(retry_on_server_error).with_backoff(backoff))
402                .into_layer(service_fn(move |req: Request<RetryBody>| {
403                    let inner_calls = inner_calls.clone();
404                    async move {
405                        let n = inner_calls.fetch_add(1, Ordering::SeqCst);
406                        // an inner layer marking the request writes into this
407                        // attempt's own fork, so the policy (reading the caller's
408                        // store) must not see it and must still retry the 500
409                        req.extensions().insert(DoNotRetry::default());
410                        if n == 0 {
411                            Ok::<_, BoxError>(StatusCode::INTERNAL_SERVER_ERROR.into_response())
412                        } else {
413                            Ok(StatusCode::OK.into_response())
414                        }
415                    }
416                }));
417
418        let request: Request = Request::builder().body("x".into()).unwrap();
419        let res = service.serve(request).await.unwrap();
420        assert_eq!(res.status(), StatusCode::OK);
421        assert_eq!(
422            calls.load(Ordering::SeqCst),
423            2,
424            "an inner-layer DoNotRetry insert must not suppress the retry"
425        );
426    }
427
428    #[tokio::test]
429    async fn fresh_policy_requests_keep_the_caller_extension_view() {
430        use std::sync::Arc;
431        use std::sync::atomic::{AtomicUsize, Ordering};
432
433        #[derive(Debug, Extension)]
434        struct CallerState(AtomicUsize);
435        #[derive(Debug, Extension)]
436        struct AttemptOnly;
437
438        #[derive(Clone)]
439        struct FreshRequestPolicy;
440
441        impl Policy<Response, BoxError> for FreshRequestPolicy {
442            async fn retry(
443                &self,
444                req: Request<RetryBody>,
445                result: Result<Response, BoxError>,
446            ) -> PolicyResult<Response, BoxError> {
447                assert!(!req.extensions().contains::<AttemptOnly>());
448                let call = req
449                    .extensions()
450                    .get_ref::<CallerState>()
451                    .expect("every policy round sees the caller store")
452                    .0
453                    .fetch_add(1, Ordering::SeqCst);
454                if call < 2 {
455                    PolicyResult::Retry {
456                        req: Request::new(RetryBody::empty()),
457                    }
458                } else {
459                    PolicyResult::Abort(result)
460                }
461            }
462
463            fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
464                Some(req.clone())
465            }
466        }
467
468        let attempts = Arc::new(AtomicUsize::new(0));
469        let inner_attempts = attempts.clone();
470        let service = RetryLayer::new(FreshRequestPolicy).into_layer(service_fn(
471            move |req: Request<RetryBody>| {
472                let inner_attempts = inner_attempts.clone();
473                async move {
474                    assert!(req.extensions().contains::<CallerState>());
475                    req.extensions().insert(AttemptOnly);
476                    inner_attempts.fetch_add(1, Ordering::SeqCst);
477                    Ok::<_, BoxError>(StatusCode::INTERNAL_SERVER_ERROR.into_response())
478                }
479            },
480        ));
481
482        let caller_extensions = Extensions::new();
483        caller_extensions.insert(CallerState(AtomicUsize::new(0)));
484        let mut request = Request::new(RetryBody::empty());
485        request.set_extensions(caller_extensions.clone());
486
487        let response = service.serve(request).await.unwrap();
488        assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
489        assert_eq!(attempts.load(Ordering::SeqCst), 3);
490        assert_eq!(
491            caller_extensions
492                .get_ref::<CallerState>()
493                .unwrap()
494                .0
495                .load(Ordering::SeqCst),
496            3
497        );
498        assert!(!caller_extensions.contains::<AttemptOnly>());
499    }
500}