Skip to main content

timvw_rust_runtime/
timeout.rs

1//! A request timeout that works natively and on WebAssembly: [`TimeoutLayer`].
2//!
3//! tower's own `TimeoutLayer` sleeps with tokio, which has no timers on WebAssembly (`wasm32-unknown-unknown`), and
4//! `fetch` has no timeout of its own. This one waits with the timer the rest of the crate uses: tokio's natively (so
5//! tests can pause time), the JavaScript host's `setTimeout` on WebAssembly.
6//!
7//! Where it sits decides what it bounds. [`Client::layer`](crate::Client::layer) wraps the current stack, so a timeout
8//! added to `Client::default()` bounds the whole call, retries and their waits included. Built from the bare service,
9//! a timeout added before the retries bounds each attempt:
10//!
11//! ```no_run
12//! # #[cfg(all(feature = "reqwest", feature = "retry"))]
13//! # fn demo() {
14//! use std::time::Duration;
15//! use timvw_rust_runtime::retry::{RetryLayer, RetryPolicy};
16//! use timvw_rust_runtime::timeout::TimeoutLayer;
17//! use timvw_rust_runtime::{Client, ReqwestService};
18//!
19//! // The whole call, retries included.
20//! let client = Client::default().layer(TimeoutLayer::new(Duration::from_secs(120)));
21//!
22//! // Each attempt, retried when it times out; and the whole call.
23//! let client = Client::from_service(ReqwestService::default())
24//!     .layer(TimeoutLayer::new(Duration::from_secs(30)))
25//!     .layer(RetryLayer::new(RetryPolicy::default().retry_timeouts(true)))
26//!     .layer(TimeoutLayer::new(Duration::from_secs(120)));
27//! # }
28//! ```
29//!
30//! The timeout runs until the response's status and headers arrive: reading the body is not part of it. A request
31//! that times out is dropped, which cancels it (reqwest aborts the `fetch` on WebAssembly), and fails with an
32//! [`Error`](crate::Error) whose [`is_timeout`](crate::Error::is_timeout) is true.
33
34use std::future::Future;
35use std::pin::Pin;
36use std::task::{Context, Poll};
37use std::time::Duration;
38
39use tower::{Layer, Service};
40
41use crate::error::TimedOut;
42use crate::timer::{Instant, Sleep};
43use crate::BoxError;
44
45/// Fails a request whose response does not arrive within a time limit; see the [module](self).
46#[derive(Clone, Copy, Debug)]
47pub struct TimeoutLayer {
48    timeout: Duration,
49}
50
51impl TimeoutLayer {
52    /// A timeout of `timeout`, counted from when the request is sent.
53    pub fn new(timeout: Duration) -> Self {
54        Self { timeout }
55    }
56}
57
58impl<S> Layer<S> for TimeoutLayer {
59    type Service = Timeout<S>;
60
61    fn layer(&self, inner: S) -> Self::Service {
62        Timeout {
63            inner,
64            timeout: self.timeout,
65        }
66    }
67}
68
69/// The service of a [`TimeoutLayer`].
70#[derive(Clone, Debug)]
71pub struct Timeout<S> {
72    inner: S,
73    timeout: Duration,
74}
75
76impl<S> Timeout<S> {
77    /// Wraps `inner` in a timeout of `timeout`.
78    pub fn new(inner: S, timeout: Duration) -> Self {
79        Self { inner, timeout }
80    }
81}
82
83impl<S, R> Service<R> for Timeout<S>
84where
85    S: Service<R>,
86    S::Error: Into<BoxError>,
87{
88    type Response = S::Response;
89    type Error = BoxError;
90    type Future = ResponseFuture<S::Future>;
91
92    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
93        self.inner.poll_ready(cx).map_err(Into::into)
94    }
95
96    fn call(&mut self, request: R) -> Self::Future {
97        let deadline = Instant::now() + self.timeout;
98        ResponseFuture {
99            inner: Box::pin(self.inner.call(request)),
100            sleep: Sleep::until(deadline),
101            timeout: self.timeout,
102        }
103    }
104}
105
106/// The future of a [`Timeout`] service.
107pub struct ResponseFuture<F> {
108    inner: Pin<Box<F>>,
109    sleep: Sleep,
110    timeout: Duration,
111}
112
113impl<F, T, E> Future for ResponseFuture<F>
114where
115    F: Future<Output = Result<T, E>>,
116    E: Into<BoxError>,
117{
118    type Output = Result<T, BoxError>;
119
120    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
121        if let Poll::Ready(result) = self.inner.as_mut().poll(cx) {
122            return Poll::Ready(result.map_err(Into::into));
123        }
124        match Pin::new(&mut self.sleep).poll(cx) {
125            Poll::Ready(()) => Poll::Ready(Err(Box::new(TimedOut(format!(
126                "request timed out after {:?}",
127                self.timeout
128            ))))),
129            Poll::Pending => Poll::Pending,
130        }
131    }
132}
133
134impl<F> std::fmt::Debug for ResponseFuture<F> {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        f.debug_struct("ResponseFuture")
137            .field("timeout", &self.timeout)
138            .finish_non_exhaustive()
139    }
140}
141
142#[cfg(all(test, not(target_family = "wasm")))]
143mod tests {
144    use super::*;
145    use tower::ServiceExt;
146
147    #[tokio::test(start_paused = true)]
148    async fn a_slow_service_times_out_and_a_fast_one_does_not() {
149        let slow = tower::service_fn(|delay: Duration| async move {
150            tokio::time::sleep(delay).await;
151            Ok::<_, BoxError>(delay)
152        });
153        let service = TimeoutLayer::new(Duration::from_secs(1)).layer(slow);
154        let fast = service
155            .clone()
156            .oneshot(Duration::from_millis(999))
157            .await
158            .unwrap();
159        assert_eq!(fast, Duration::from_millis(999));
160        let started = Instant::now();
161        let error = service.oneshot(Duration::from_secs(5)).await.unwrap_err();
162        assert_eq!(started.elapsed(), Duration::from_secs(1));
163        assert!(error.is::<TimedOut>(), "{error}");
164        assert_eq!(error.to_string(), "request timed out after 1s");
165    }
166
167    #[tokio::test(start_paused = true)]
168    async fn the_service_error_passes_through() {
169        let failing = tower::service_fn(|()| async { Err::<(), _>(BoxError::from("refused")) });
170        let error = TimeoutLayer::new(Duration::from_secs(1))
171            .layer(failing)
172            .oneshot(())
173            .await
174            .unwrap_err();
175        assert_eq!(error.to_string(), "refused");
176    }
177}