Skip to main content

rama_http/layer/retry/
policy.rs

1use super::RetryBody;
2use crate::Request;
3
4/// A "retry policy" to classify if a request should be retried.
5///
6/// # Example
7///
8/// ```
9/// use rama_http::Request;
10/// use rama_http::layer::retry::{Policy, PolicyResult, RetryBody};
11/// use std::sync::Arc;
12/// use parking_lot::Mutex;
13///
14/// struct Attempts(Arc<Mutex<usize>>);
15///
16/// impl<R, E> Policy< R, E> for Attempts
17///     where
18///         R: Send + 'static,
19///         E: Send + Sync + 'static,
20/// {
21///     async fn retry(&self, req: Request<RetryBody>, result: Result<R, E>) -> PolicyResult<R, E> {
22///         match result {
23///             Ok(_) => {
24///                 // Treat all `Response`s as success,
25///                 // so don't retry...
26///                 PolicyResult::Abort(result)
27///             },
28///             Err(_) => {
29///                 // Treat all errors as failures...
30///                 // But we limit the number of attempts...
31///                 let mut attempts = self.0.lock();
32///                 if *attempts > 0 {
33///                     // Try again!
34///                     *attempts -= 1;
35///                     PolicyResult::Retry { req }
36///                 } else {
37///                     // Used all our attempts, no retry...
38///                     PolicyResult::Abort(result)
39///                 }
40///             }
41///         }
42///     }
43///
44///     fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
45///         Some(req.clone())
46///     }
47/// }
48/// ```
49pub trait Policy<R, E>: Send + Sync + 'static {
50    /// Check the policy if a certain request should be retried.
51    ///
52    /// This method is passed a reference to the original request, and either
53    /// the [`Service::Output`] or [`Service::Error`] from the inner service.
54    ///
55    /// If the request should **not** be retried, return `None`.
56    ///
57    /// If the request *should* be retried, return `Some` future that will delay
58    /// the next retry of the request. This can be used to sleep for a certain
59    /// duration, to wait for some external condition to be met before retrying,
60    /// or resolve right away, if the request should be retried immediately.
61    ///
62    /// ## Mutating Requests
63    ///
64    /// The policy MAY chose to mutate the `req`: if the request is mutated, the
65    /// mutated request will be sent to the inner service in the next retry.
66    /// This can be helpful for use cases like tracking the retry count in a
67    /// header. Mutate the handed-in request *in place* — its
68    /// [`Extensions`](rama_core::extensions::Extensions) are the caller's
69    /// store, so recorded metadata both reaches the next attempt and stays
70    /// visible to the caller after the call. Returning a freshly built request
71    /// instead keeps its body and headers but not its extensions: each attempt
72    /// is re-forked from the caller's store (see the
73    /// [module docs](crate::layer::retry)).
74    ///
75    /// ## Mutating Results
76    ///
77    /// The policy MAY chose to mutate the result. This enables the retry
78    /// policy to convert a failure into a success and vice versa. For example,
79    /// if the policy is used to poll while waiting for a state change, the
80    /// policy can switch the result to emit a specific error when retries are
81    /// exhausted.
82    ///
83    /// As above, record such metadata on the handed-in request (e.g. the
84    /// number of retries required, or that a failure exhausted all retries).
85    ///
86    /// [`Service::Output`]: rama_core::Service::Output
87    /// [`Service::Error`]: rama_core::Service::Error
88    fn retry(
89        &self,
90
91        req: Request<RetryBody>,
92        result: Result<R, E>,
93    ) -> impl Future<Output = PolicyResult<R, E>> + Send + '_;
94
95    /// Tries to clone a request before being passed to the inner service.
96    ///
97    /// If the request cannot be cloned, return [`None`]. Moreover, the retry
98    /// function will not be called if the [`None`] is returned.
99    fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>>;
100}
101
102impl<P, R, E> Policy<R, E> for &'static P
103where
104    P: Policy<R, E>,
105{
106    #[inline(always)]
107    fn retry(
108        &self,
109        req: Request<RetryBody>,
110        result: Result<R, E>,
111    ) -> impl Future<Output = PolicyResult<R, E>> + Send + '_ {
112        (**self).retry(req, result)
113    }
114
115    #[inline(always)]
116    fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
117        (**self).clone_input(req)
118    }
119}
120
121impl<P, R, E> Policy<R, E> for std::sync::Arc<P>
122where
123    P: Policy<R, E>,
124{
125    #[inline(always)]
126    fn retry(
127        &self,
128
129        req: Request<RetryBody>,
130        result: Result<R, E>,
131    ) -> impl Future<Output = PolicyResult<R, E>> + Send + '_ {
132        (**self).retry(req, result)
133    }
134
135    #[inline(always)]
136    fn clone_input(&self, req: &Request<RetryBody>) -> Option<Request<RetryBody>> {
137        (**self).clone_input(req)
138    }
139}
140
141// TODO revisit PolicyResult after we remove Context concept
142// and see to be smarter about async fns in general
143
144/// The full result of a limit policy.
145#[expect(
146    clippy::large_enum_variant,
147    reason = "retry hands the request back without adding an allocation"
148)]
149pub enum PolicyResult<R, E> {
150    /// The result should not be retried,
151    /// and the result should be returned to the caller.
152    Abort(Result<R, E>),
153    /// The result should be retried,
154    /// and the request should be passed to the inner service again.
155    Retry {
156        /// The request to be retried, with the above context.
157        req: Request<RetryBody>,
158    },
159}
160
161impl<R, E> std::fmt::Debug for PolicyResult<R, E>
162where
163    R: std::fmt::Debug,
164    E: std::fmt::Debug,
165{
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        match self {
168            Self::Abort(err) => write!(f, "PolicyResult::Abort({err:?})"),
169            Self::Retry { req } => {
170                write!(f, "PolicyResult::Retry {{ req: {req:?} }}",)
171            }
172        }
173    }
174}
175
176macro_rules! impl_retry_policy_either {
177    ($id:ident, $($param:ident),+ $(,)?) => {
178        impl<$($param),+, Response, Error> Policy< Response, Error> for rama_core::combinators::$id<$($param),+>
179        where
180            $($param: Policy< Response, Error>),+,
181
182            Response: Send + 'static,
183            Error: Send + 'static,
184        {
185            async fn retry(
186                &self,
187                req: rama_http_types::Request<RetryBody>,
188                result: Result<Response, Error>,
189            ) -> PolicyResult< Response, Error> {
190                match self {
191                    $(
192                        rama_core::combinators::$id::$param(policy) => policy.retry(req, result).await,
193                    )+
194                }
195            }
196
197            fn clone_input(
198                &self,
199                req: &rama_http_types::Request<RetryBody>,
200            ) -> Option<rama_http_types::Request<RetryBody>> {
201                match self {
202                    $(
203                        rama_core::combinators::$id::$param(policy) => policy.clone_input(req),
204                    )+
205                }
206            }
207        }
208    };
209}
210
211rama_core::combinators::impl_either!(impl_retry_policy_either);