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