Skip to main content

ntex_util/services/
retry.rs

1use ntex_service::{Ctx, Middleware, Service};
2
3/// Determines whether and how a failed service call is retried.
4pub trait Policy<S: Service<St, Req>, St, Req>: Sized + Clone {
5    /// Returns whether the call should be retried.
6    async fn retry(&mut self, req: &Req, res: &Result<S::Res, S::Error>) -> bool;
7
8    /// Clones or reconstructs a request for a possible retry.
9    ///
10    /// Returning `None` prevents retries for this request.
11    fn clone_request(&self, req: &Req) -> Option<Req>;
12}
13
14#[derive(Clone, Debug)]
15/// Middleware that retries service calls according to a [`Policy`].
16///
17/// The policy is cloned for each request.
18pub struct Retry<P> {
19    policy: P,
20}
21
22#[derive(Clone, Debug)]
23/// A service that retries calls according to a [`Policy`].
24pub struct RetryService<P, S> {
25    policy: P,
26    service: S,
27}
28
29impl<P> Retry<P> {
30    /// Creates retry middleware with the specified policy.
31    pub fn new(policy: P) -> Self {
32        Retry { policy }
33    }
34}
35
36impl<P: Clone, S, St> Middleware<S, St> for Retry<P> {
37    type Service = RetryService<P, S>;
38
39    fn create(&self, _: &St, service: S) -> Self::Service {
40        RetryService {
41            service,
42            policy: self.policy.clone(),
43        }
44    }
45}
46
47impl<P, S> RetryService<P, S> {
48    /// Wraps a service with the specified retry policy.
49    pub fn new(policy: P, service: S) -> Self {
50        RetryService { policy, service }
51    }
52}
53
54impl<P, S, St, Req> Service<St, Req> for RetryService<P, S>
55where
56    P: Policy<S, St, Req>,
57    S: Service<St, Req>,
58{
59    type Res = S::Res;
60    type Error = S::Error;
61
62    async fn call(&self, req: Req, ctx: Ctx<'_, Self, St>) -> Result<S::Res, S::Error> {
63        let mut policy = self.policy.clone();
64        let mut cloned = policy.clone_request(&req);
65        let mut result = ctx.call(&self.service, req).await;
66
67        while let Some(r) = cloned.take() {
68            if !policy.retry(&r, &result).await {
69                break;
70            }
71            cloned = policy.clone_request(&r);
72            result = ctx.call(&self.service, r).await;
73        }
74        result
75    }
76
77    ntex_service::forward!(St, service);
78}
79
80#[derive(Copy, Clone, Debug)]
81/// A retry policy that retries every service error.
82///
83/// The default policy permits up to three retries after the initial call.
84pub struct DefaultRetryPolicy(u16);
85
86impl DefaultRetryPolicy {
87    /// Creates a policy that permits up to `retry` retries.
88    pub fn new(retry: u16) -> Self {
89        DefaultRetryPolicy(retry)
90    }
91}
92
93impl Default for DefaultRetryPolicy {
94    fn default() -> Self {
95        DefaultRetryPolicy::new(3)
96    }
97}
98
99impl<S, St, Req> Policy<S, St, Req> for DefaultRetryPolicy
100where
101    S: Service<St, Req>,
102    Req: Clone,
103{
104    async fn retry(&mut self, _: &Req, res: &Result<S::Res, S::Error>) -> bool {
105        if res.is_err() {
106            if self.0 == 0 {
107                false
108            } else {
109                self.0 -= 1;
110                true
111            }
112        } else {
113            false
114        }
115    }
116
117    fn clone_request(&self, req: &Req) -> Option<Req> {
118        Some(req.clone())
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    #![allow(clippy::unused_async_trait_impl)]
125    use std::{cell::Cell, rc::Rc};
126
127    use ntex_service::{Pipeline, apply, fn_factory};
128
129    use super::*;
130
131    #[derive(Clone, Debug, PartialEq)]
132    struct TestService(Rc<Cell<usize>>);
133
134    impl Service<(), ()> for TestService {
135        type Res = ();
136        type Error = ();
137
138        async fn call(&self, _r: (), _: Ctx<'_, Self>) -> Result<(), ()> {
139            let cnt = self.0.get();
140            if cnt == 0 {
141                Ok(())
142            } else {
143                self.0.set(cnt - 1);
144                Err(())
145            }
146        }
147    }
148
149    /// STATEMENT: Retries stop at the configured budget or return an earlier success.
150    ///
151    /// HOW:
152    /// - Exhaust the default policy against a counted failing service and check remaining failures.
153    /// - Create middleware services that succeed within the retry budget and verify success.
154    #[ntex::test]
155    async fn test_retry() {
156        let cnt = Rc::new(Cell::new(5));
157        let svc = Pipeline::new(
158            (),
159            RetryService::new(DefaultRetryPolicy::default(), TestService(cnt.clone())).clone(),
160        );
161        assert_eq!(svc.call(()).await, Err(()));
162        assert_eq!(svc.ready().await, Ok(()));
163        svc.shutdown().await;
164        assert_eq!(cnt.get(), 1);
165
166        let factory = apply(
167            Retry::new(DefaultRetryPolicy::new(3)).clone(),
168            fn_factory(|(): &()| async { Ok::<_, ()>(TestService(Rc::new(Cell::new(2)))) }),
169        );
170        let srv = factory.pipeline(()).await.unwrap();
171        assert_eq!(srv.call(()).await, Ok(()));
172
173        let factory = apply(
174            Retry::new(DefaultRetryPolicy::new(3)).clone(),
175            fn_factory(|(): &()| async { Ok::<_, ()>(TestService(Rc::new(Cell::new(2)))) }),
176        );
177        let srv = factory.pipeline(()).await.unwrap();
178        assert_eq!(srv.call(()).await, Ok(()));
179    }
180
181    #[derive(Clone)]
182    struct CloneOnce(Rc<Cell<usize>>);
183
184    impl Policy<TestService, (), ()> for CloneOnce {
185        async fn retry(&mut self, (): &(), res: &Result<(), ()>) -> bool {
186            res.is_err()
187        }
188
189        fn clone_request(&self, (): &()) -> Option<()> {
190            let n = self.0.get();
191            self.0.set(n + 1);
192            if n == 0 { Some(()) } else { None }
193        }
194    }
195
196    /// STATEMENT: A retry returns its result when the request cannot be cloned again.
197    ///
198    /// HOW:
199    /// - Use a policy that permits only one request clone against a failing service.
200    /// - Verify the error, two service calls, and two clone attempts.
201    #[ntex::test]
202    async fn test_retry_without_clone() {
203        // the retried request cannot be cloned again, its result is returned
204        let cnt = Rc::new(Cell::new(5));
205        let clones = Rc::new(Cell::new(0));
206        let svc = Pipeline::new(
207            (),
208            RetryService::new(CloneOnce(clones.clone()), TestService(cnt.clone())),
209        );
210        assert_eq!(svc.call(()).await, Err(()));
211        assert_eq!(cnt.get(), 3);
212        assert_eq!(clones.get(), 2);
213    }
214}