Skip to main content

rama_http/layer/retry/
managed.rs

1//! Managed retry [`Policy`].
2//!
3//! See [`ManagedPolicy`] for more details.
4//!
5//! [`Policy`]: super::Policy
6
7use super::{Policy, PolicyResult, RetryBody};
8use crate::{Request, Response};
9use rama_core::extensions::{Extension, ExtensionsRef};
10use rama_core::telemetry::tracing;
11use rama_utils::backoff::Backoff;
12
13#[derive(Debug, Clone, Default, Extension)]
14#[extension(tags(http))]
15/// A metadata value that can be added to the [`Extensions`]
16/// of a [`Request`] to signal that the request should not be retried.
17///
18/// This requires the [`ManagedPolicy`] to be used.
19///
20/// Only the caller can set it, i.e. anything _outside_ [`Retry`]: the policy reads it off the
21/// request clone, which references the caller's store, so an insert made by an inner layer lands in
22/// that attempt's own fork and goes unseen here.
23///
24/// [`Extensions`]: rama_core::extensions::Extensions
25/// [`Retry`]: super::Retry
26#[non_exhaustive]
27pub struct DoNotRetry;
28
29/// A managed retry [`Policy`],
30/// which allows for an easier interface to configure retrying requests.
31///
32/// [`DoNotRetry`] can be added to the [`Extensions`] of a [`Request`]
33/// to signal that the request should not be retried, regardless
34/// of the retry functionality defined.
35///
36/// [`Extensions`]: rama_core::extensions::Extensions
37#[derive(Debug, Clone)]
38pub struct ManagedPolicy<B = Undefined, C = Undefined, R = Undefined> {
39    backoff: B,
40    clone: C,
41    retry: R,
42}
43
44impl<B, C, R, Response, Error> Policy<Response, Error> for ManagedPolicy<B, C, R>
45where
46    B: Backoff,
47    C: CloneInput,
48    R: RetryRule<Request<RetryBody>, Response, Error>,
49    Response: Send + 'static,
50    Error: Send + 'static,
51{
52    async fn retry(
53        &self,
54        req: Request<RetryBody>,
55        result: Result<Response, Error>,
56    ) -> PolicyResult<Response, Error> {
57        if req.extensions().get_ref::<DoNotRetry>().is_some() {
58            // Custom extension to signal that the request should not be retried.
59            return PolicyResult::Abort(result);
60        }
61
62        let (req, result, retry) = self.retry.retry(req, result).await;
63        if retry && self.backoff.next_backoff().await {
64            PolicyResult::Retry { req }
65        } else {
66            self.backoff.reset().await;
67            PolicyResult::Abort(result)
68        }
69    }
70
71    fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
72        if req.extensions().get_ref::<DoNotRetry>().is_some() {
73            None
74        } else {
75            self.clone.clone_input(req)
76        }
77    }
78}
79
80impl Default for ManagedPolicy<Undefined, Undefined, Undefined> {
81    fn default() -> Self {
82        Self {
83            backoff: Undefined,
84            clone: Undefined,
85            retry: Undefined,
86        }
87    }
88}
89
90impl<F> ManagedPolicy<Undefined, Undefined, F> {
91    /// Create a new [`ManagedPolicy`] which uses the provided
92    /// function to determine if a request should be retried.
93    ///
94    /// The default cloning is used and no backoff is applied.
95    #[inline]
96    pub fn new(retry: F) -> Self {
97        ManagedPolicy::default().with_retry(retry)
98    }
99}
100
101impl<C, R> ManagedPolicy<Undefined, C, R> {
102    /// add a backoff to this [`ManagedPolicy`].
103    pub fn with_backoff<B>(self, backoff: B) -> ManagedPolicy<B, C, R> {
104        ManagedPolicy {
105            backoff,
106            clone: self.clone,
107            retry: self.retry,
108        }
109    }
110}
111
112impl<B, R> ManagedPolicy<B, Undefined, R> {
113    /// add a cloning function to this [`ManagedPolicy`].
114    /// to determine if a request should be cloned
115    pub fn with_clone<C>(self, clone: C) -> ManagedPolicy<B, C, R> {
116        ManagedPolicy {
117            backoff: self.backoff,
118            clone,
119            retry: self.retry,
120        }
121    }
122}
123
124impl<B, C> ManagedPolicy<B, C, Undefined> {
125    /// add a retry function to this [`ManagedPolicy`].
126    /// to determine if a request should be retried.
127    pub fn with_retry<R>(self, retry: R) -> ManagedPolicy<B, C, R> {
128        ManagedPolicy {
129            backoff: self.backoff,
130            clone: self.clone,
131            retry,
132        }
133    }
134}
135
136/// A trait that is used to umbrella-cover all possible
137/// implementation kinds for the retry rule functionality.
138pub trait RetryRule<Request, R, E>:
139    private::Sealed<(Request, R, E)> + Send + Sync + 'static
140{
141    /// Check if the given result should be retried.
142    fn retry(
143        &self,
144        request: Request,
145        result: Result<R, E>,
146    ) -> impl Future<Output = (Request, Result<R, E>, bool)> + Send + '_;
147}
148
149impl<Request, Body, E> RetryRule<Request, Response<Body>, E> for Undefined
150where
151    E: std::fmt::Debug + Send + Sync + 'static,
152    Body: Send + 'static,
153    Request: ExtensionsRef + Send + 'static,
154{
155    async fn retry(
156        &self,
157        request: Request,
158        result: Result<Response<Body>, E>,
159    ) -> (Request, Result<Response<Body>, E>, bool) {
160        match &result {
161            Ok(response) => {
162                let status = response.status();
163                if status.is_server_error() {
164                    tracing::debug!(
165                        "retrying server error http status code: {status} ({})",
166                        status.as_u16()
167                    );
168                    (request, result, true)
169                } else {
170                    (request, result, false)
171                }
172            }
173            Err(error) => {
174                tracing::debug!("retrying error: {:?}", error);
175                (request, result, true)
176            }
177        }
178    }
179}
180
181impl<F, Fut, Request, R, E> RetryRule<Request, R, E> for F
182where
183    F: Fn(Request, Result<R, E>) -> Fut + Send + Sync + 'static,
184    Fut: Future<Output = (Request, Result<R, E>, bool)> + Send + 'static,
185    Request: Send + 'static,
186    R: Send + 'static,
187    E: Send + Sync + 'static,
188{
189    async fn retry(&self, request: Request, result: Result<R, E>) -> (Request, Result<R, E>, bool) {
190        self(request, result).await
191    }
192}
193
194/// A trait that is used to umbrella-cover all possible
195/// implementation kinds for the cloning functionality.
196pub trait CloneInput: private::Sealed<()> + Send + Sync + 'static {
197    /// Clone the input request if necessary.
198    ///
199    /// See [`Policy::clone_input`] for more details.
200    ///
201    /// [`Policy::clone_input`]: super::Policy::clone_input
202    fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>>;
203}
204
205impl CloneInput for Undefined {
206    fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
207        Some(req.clone())
208    }
209}
210
211impl<F> CloneInput for F
212where
213    F: Fn(&Request<RetryBody>) -> Option<Request<RetryBody>> + Send + Sync + 'static,
214{
215    fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
216        self(req)
217    }
218}
219
220#[derive(Debug, Clone)]
221#[non_exhaustive]
222/// A type to represent the undefined default type,
223/// which is used as the placeholder in the [`ManagedPolicy`],
224/// when the user does not provide a specific type.
225pub struct Undefined;
226
227impl std::fmt::Display for Undefined {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        write!(f, "Undefined")
230    }
231}
232
233impl Backoff for Undefined {
234    async fn next_backoff(&self) -> bool {
235        true
236    }
237
238    async fn reset(&self) {}
239}
240
241mod private {
242    use super::*;
243
244    pub trait Sealed<S> {}
245
246    impl<S> Sealed<S> for Undefined {}
247    impl<F> Sealed<()> for F where
248        F: Fn(&Request<RetryBody>) -> Option<Request<RetryBody>> + Send + Sync + 'static
249    {
250    }
251    impl<F, Fut, Request, R, E> Sealed<(Request, R, E)> for F
252    where
253        F: Fn(Request, Result<R, E>) -> Fut + Send + Sync + 'static,
254        Fut: Future<Output = (Request, Result<R, E>, bool)> + Send + 'static,
255    {
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::{StatusCode, service::web::response::IntoResponse};
263    use rama_core::extensions::ExtensionsRef;
264    use rama_utils::{backoff::ExponentialBackoff, rng::HasherRng};
265    use std::time::Duration;
266
267    fn assert_clone_input_none(req: &Request<RetryBody>, policy: &impl Policy<Response, ()>) {
268        assert!(policy.clone_input(req).is_none());
269    }
270
271    fn assert_clone_input_some(req: &Request<RetryBody>, policy: &impl Policy<Response, ()>) {
272        assert!(policy.clone_input(req).is_some());
273    }
274
275    async fn assert_retry(
276        req: Request<RetryBody>,
277        result: Result<Response, ()>,
278        policy: &impl Policy<Response, ()>,
279    ) {
280        match policy.retry(req, result).await {
281            PolicyResult::Retry { .. } => (),
282            PolicyResult::Abort(_) => panic!("expected retry"),
283        };
284    }
285
286    async fn assert_abort(
287        req: Request<RetryBody>,
288        result: Result<Response, ()>,
289        policy: &impl Policy<Response, ()>,
290    ) {
291        match policy.retry(req, result).await {
292            PolicyResult::Retry { .. } => panic!("expected abort"),
293            PolicyResult::Abort(_) => (),
294        };
295    }
296
297    #[tokio::test]
298    async fn managed_policy_default() {
299        let request = Request::builder()
300            .method("GET")
301            .uri("http://example.com")
302            .body(RetryBody::empty())
303            .unwrap();
304
305        let policy = ManagedPolicy::default();
306
307        assert_clone_input_some(&request, &policy);
308
309        // do not retry HTTP Ok
310        assert_abort(request.clone(), Ok(StatusCode::OK.into_response()), &policy).await;
311
312        // do retry HTTP InternalServerError
313        assert_retry(
314            request.clone(),
315            Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
316            &policy,
317        )
318        .await;
319
320        // also retry any error case
321        assert_retry(request, Err(()), &policy).await;
322    }
323
324    #[tokio::test]
325    async fn managed_policy_default_do_not_retry() {
326        let req = Request::builder()
327            .method("GET")
328            .uri("http://example.com")
329            .body(RetryBody::empty())
330            .unwrap();
331
332        let policy = ManagedPolicy::default();
333
334        req.extensions().insert(DoNotRetry);
335
336        assert_clone_input_none(&req, &policy);
337
338        // do not retry HTTP Ok (.... Of course)
339        assert_abort(req.clone(), Ok(StatusCode::OK.into_response()), &policy).await;
340
341        // do not retry HTTP InternalServerError
342        assert_abort(
343            req.clone(),
344            Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
345            &policy,
346        )
347        .await;
348
349        // also do not retry any error case
350        assert_abort(req, Err(()), &policy).await;
351    }
352
353    #[tokio::test]
354    async fn test_policy_custom_clone_fn() {
355        let req = Request::builder()
356            .method("GET")
357            .uri("http://example.com")
358            .body(RetryBody::empty())
359            .unwrap();
360
361        fn clone_fn(_: &Request<RetryBody>) -> Option<Request<RetryBody>> {
362            None
363        }
364
365        let policy = ManagedPolicy::default().with_clone(clone_fn);
366
367        assert_clone_input_none(&req, &policy);
368
369        // retry should still be the default
370        assert_abort(req, Ok(StatusCode::OK.into_response()), &policy).await;
371    }
372
373    #[test]
374    fn custom_clone_result_and_placeholder_display_are_preserved() {
375        let req = Request::builder()
376            .uri("http://example.com/original")
377            .body(RetryBody::empty())
378            .unwrap();
379        let clone = |req: &Request<RetryBody>| {
380            let mut cloned = req.clone();
381            *cloned.uri_mut() = "http://example.com/cloned".try_into().unwrap();
382            Some(cloned)
383        };
384
385        let cloned = CloneInput::clone_input(&clone, &req).unwrap();
386        assert_eq!(cloned.uri().as_str(), "http://example.com/cloned");
387        assert_eq!(Undefined.to_string(), "Undefined");
388    }
389
390    #[tokio::test]
391    async fn test_policy_custom_retry_fn() {
392        let req = Request::builder()
393            .method("GET")
394            .uri("http://example.com")
395            .body(RetryBody::empty())
396            .unwrap();
397
398        async fn retry_fn<Body, R, E>(
399            request: Request<Body>,
400            result: Result<R, E>,
401        ) -> (Request<Body>, Result<R, E>, bool) {
402            match result {
403                Ok(_) => (request, result, false),
404                Err(_) => (request, result, true),
405            }
406        }
407
408        let policy = ManagedPolicy::new(retry_fn);
409
410        // default clone should be used
411        assert_clone_input_some(&req, &policy);
412
413        // retry should be the custom one
414        assert_abort(req.clone(), Ok(StatusCode::OK.into_response()), &policy).await;
415        assert_abort(
416            req.clone(),
417            Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
418            &policy,
419        )
420        .await;
421        assert_retry(req, Err(()), &policy).await;
422    }
423
424    #[tokio::test]
425    async fn test_policy_fully_custom() {
426        let req = Request::builder()
427            .method("GET")
428            .uri("http://example.com")
429            .body(RetryBody::empty())
430            .unwrap();
431
432        fn clone_fn(_: &Request<RetryBody>) -> Option<Request<RetryBody>> {
433            None
434        }
435
436        async fn retry_fn<Body, R, E>(
437            req: Request<Body>,
438            result: Result<R, E>,
439        ) -> (Request<Body>, Result<R, E>, bool) {
440            match result {
441                Ok(_) => (req, result, false),
442                Err(_) => (req, result, true),
443            }
444        }
445
446        let backoff = ExponentialBackoff::new(
447            Duration::from_millis(1),
448            Duration::from_millis(5),
449            0.1,
450            HasherRng::default,
451        )
452        .unwrap();
453
454        let policy = ManagedPolicy::default()
455            .with_backoff(backoff)
456            .with_clone(clone_fn)
457            .with_retry(retry_fn);
458
459        assert_clone_input_none(&req, &policy);
460
461        // retry should be the custom one
462        assert_abort(req.clone(), Ok(StatusCode::OK.into_response()), &policy).await;
463        assert_abort(
464            req.clone(),
465            Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()),
466            &policy,
467        )
468        .await;
469        assert_retry(req, Err(()), &policy).await;
470    }
471}