Skip to main content

ntex_util/services/
timeout.rs

1//! Service that applies a timeout to requests.
2//!
3//! If the response does not complete within the specified timeout, the response
4//! will be aborted.
5use std::{fmt, marker::PhantomData};
6
7use ntex_service::{Ctx, IntoService, Middleware, Service};
8
9use crate::future::{Either, select};
10use crate::time::{Millis, sleep};
11
12/// Applies a timeout to requests.
13///
14/// Timeout transform is disabled if timeout is set to 0
15#[derive(Debug)]
16pub struct Timeout<St> {
17    timeout: Millis,
18    _t: PhantomData<St>,
19}
20
21/// Timeout error
22pub enum TimeoutError<E> {
23    /// Service error
24    Service(E),
25    /// Service call timeout
26    Timeout,
27}
28
29impl<E> From<E> for TimeoutError<E> {
30    fn from(err: E) -> Self {
31        TimeoutError::Service(err)
32    }
33}
34
35impl<E: fmt::Debug> fmt::Debug for TimeoutError<E> {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            TimeoutError::Service(e) => write!(f, "TimeoutError::Service({e:?})"),
39            TimeoutError::Timeout => write!(f, "TimeoutError::Timeout"),
40        }
41    }
42}
43
44impl<E: fmt::Display> fmt::Display for TimeoutError<E> {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            TimeoutError::Service(e) => e.fmt(f),
48            TimeoutError::Timeout => write!(f, "Service call timeout"),
49        }
50    }
51}
52
53impl<E: fmt::Display + fmt::Debug> std::error::Error for TimeoutError<E> {}
54
55impl<E: PartialEq> PartialEq for TimeoutError<E> {
56    fn eq(&self, other: &TimeoutError<E>) -> bool {
57        match self {
58            TimeoutError::Service(e1) => match other {
59                TimeoutError::Service(e2) => e1 == e2,
60                TimeoutError::Timeout => false,
61            },
62            TimeoutError::Timeout => match other {
63                TimeoutError::Service(_) => false,
64                TimeoutError::Timeout => true,
65            },
66        }
67    }
68}
69
70impl<St> Timeout<St> {
71    pub fn new<T: Into<Millis>>(timeout: T) -> Self {
72        Timeout {
73            timeout: timeout.into(),
74            _t: PhantomData,
75        }
76    }
77}
78
79impl<St> Clone for Timeout<St> {
80    fn clone(&self) -> Self {
81        Timeout {
82            timeout: self.timeout,
83            _t: PhantomData,
84        }
85    }
86}
87
88impl<S, St> Middleware<S, St> for Timeout<St> {
89    type Service = TimeoutService<S, St>;
90
91    fn create(&self, _: &St, service: S) -> Self::Service {
92        TimeoutService {
93            service,
94            timeout: self.timeout,
95            st: PhantomData,
96        }
97    }
98}
99
100/// Applies a timeout to requests.
101#[derive(Debug, Clone)]
102pub struct TimeoutService<S, St> {
103    service: S,
104    timeout: Millis,
105    st: PhantomData<St>,
106}
107
108impl<S, St> TimeoutService<S, St> {
109    pub fn new<T, Req>(timeout: T, service: impl IntoService<S, St, Req>) -> Self
110    where
111        T: Into<Millis>,
112        S: Service<St, Req>,
113    {
114        TimeoutService {
115            service: service.into_service(),
116            timeout: timeout.into(),
117            st: PhantomData,
118        }
119    }
120}
121
122impl<S, St, Req> Service<St, Req> for TimeoutService<S, St>
123where
124    S: Service<St, Req>,
125{
126    type Res = S::Res;
127    type Error = TimeoutError<S::Error>;
128
129    async fn call(&self, req: Req, ctx: Ctx<'_, Self, St>) -> Result<S::Res, Self::Error> {
130        if self.timeout.is_zero() {
131            ctx.call(&self.service, req)
132                .await
133                .map_err(TimeoutError::Service)
134        } else {
135            match select(sleep(self.timeout), ctx.call(&self.service, req)).await {
136                Either::Left(()) => Err(TimeoutError::Timeout),
137                Either::Right(res) => res.map_err(TimeoutError::Service),
138            }
139        }
140    }
141
142    ntex_service::forward_ready!(St, service, TimeoutError::Service);
143    ntex_service::forward_shutdown!(St, service);
144}
145
146#[cfg(test)]
147mod tests {
148    use std::time::Duration;
149
150    use ntex_service::{Pipeline, apply, fn_factory};
151
152    use super::*;
153
154    #[derive(Clone, Debug, PartialEq)]
155    struct SleepService(Duration);
156
157    #[derive(Clone, Debug, PartialEq)]
158    struct SrvError;
159
160    impl fmt::Display for SrvError {
161        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162            write!(f, "SrvError")
163        }
164    }
165
166    impl Service<(), ()> for SleepService {
167        type Res = ();
168        type Error = SrvError;
169
170        async fn call(&self, (): (), _: Ctx<'_, Self>) -> Result<(), SrvError> {
171            crate::time::sleep(self.0).await;
172            Ok::<_, SrvError>(())
173        }
174    }
175
176    #[ntex::test]
177    async fn test_success() {
178        let resolution = Duration::from_millis(100);
179        let wait_time = Duration::from_millis(50);
180
181        let timeout = Pipeline::new(
182            (),
183            TimeoutService::new(resolution, SleepService(wait_time)).clone(),
184        );
185        assert_eq!(timeout.call(()).await, Ok(()));
186        assert_eq!(timeout.ready().await, Ok(()));
187        timeout.shutdown().await;
188    }
189
190    #[ntex::test]
191    async fn test_zero() {
192        let wait_time = Duration::from_millis(50);
193        let resolution = Duration::from_millis(0);
194
195        let timeout = Pipeline::new((), TimeoutService::new(resolution, SleepService(wait_time)));
196        assert_eq!(timeout.call(()).await, Ok(()));
197        assert_eq!(timeout.ready().await, Ok(()));
198    }
199
200    #[ntex::test]
201    async fn test_timeout() {
202        let resolution = Duration::from_millis(100);
203        let wait_time = Duration::from_millis(500);
204
205        let timeout = Pipeline::new((), TimeoutService::new(resolution, SleepService(wait_time)));
206        assert_eq!(timeout.call(()).await, Err(TimeoutError::Timeout));
207    }
208
209    #[ntex::test]
210    #[allow(clippy::redundant_clone)]
211    async fn test_timeout_middleware() {
212        let resolution = Duration::from_millis(100);
213        let wait_time = Duration::from_millis(500);
214
215        let timeout = apply(
216            Timeout::new(resolution).clone(),
217            fn_factory(async move |()| Ok::<_, ()>(SleepService(wait_time))),
218        );
219        let srv = timeout.pipeline(()).await.unwrap();
220
221        let res = srv.call(()).await.unwrap_err();
222        assert_eq!(res, TimeoutError::Timeout);
223    }
224
225    #[test]
226    fn test_error() {
227        let err1 = TimeoutError::<SrvError>::Timeout;
228        assert!(format!("{err1:?}").contains("TimeoutError::Timeout"));
229        assert!(format!("{err1}").contains("Service call timeout"));
230
231        let err2: TimeoutError<_> = SrvError.into();
232        assert!(format!("{err2:?}").contains("TimeoutError::Service"));
233        assert!(format!("{err2}").contains("SrvError"));
234    }
235}