Skip to main content

rainier_middleware/
timeout.rs

1//! A ceiling on how long a request may take — [`Timeout`].
2//!
3//! ```ignore
4//! // Globally, from `server.request_timeout_secs`.
5//! registry.global(Timeout::seconds(30));
6//!
7//! // Or on the one route that talks to something slow.
8//! router.post("/import", import).middleware(Timeout::seconds(300));
9//! ```
10//!
11//! Without one, a handler that never returns holds its connection, its task
12//! and whatever it borrowed for as long as the process lives. Enough of those
13//! and the pool is gone; the symptom is a service that stops answering
14//! everything, with nothing in the log about the one endpoint that hung.
15//!
16//! # What cancelling actually does
17//!
18//! At the deadline the handler's future is **dropped**. Everything it holds is
19//! released and the response becomes `408`. But dropping a future only stops
20//! the part that had not run yet — work already handed to someone else keeps
21//! going:
22//!
23//! - A query in flight was already sent. The database will run it to
24//!   completion and the row will be written; only the *answer* is discarded.
25//! - An HTTP request already issued reaches the other service.
26//! - A job already pushed to the queue will be worked.
27//!
28//! So this bounds *this service's* latency. It is not a way to undo anything,
29//! and a handler that must be all-or-nothing needs a transaction, not a
30//! timeout.
31//!
32//! # It can only interrupt at an await
33//!
34//! A future is cancelled between its `await` points. A handler that blocks the
35//! thread — a long CPU loop, a synchronous file read, a `std::thread::sleep` —
36//! never yields, so the timer never gets to fire and the whole runtime worker
37//! stays blocked. Move that work to
38//! [`spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html);
39//! this middleware cannot save you from it.
40
41use std::time::Duration;
42
43use rainier_http::{IntoResponse, Request, Response};
44use rainier_support::Error;
45
46use crate::pipeline::{Middleware, Next};
47
48/// Answers `408` when the handler takes longer than `limit`.
49#[derive(Debug, Clone, Copy)]
50pub struct Timeout {
51    limit: Duration,
52}
53
54impl Timeout {
55    /// A limit of `limit`.
56    pub fn new(limit: Duration) -> Self {
57        Self { limit }
58    }
59
60    /// A limit in whole seconds — the unit a configuration file uses.
61    pub fn seconds(seconds: u64) -> Self {
62        Self::new(Duration::from_secs(seconds))
63    }
64
65    /// A limit in milliseconds, for a fast internal endpoint.
66    pub fn millis(millis: u64) -> Self {
67        Self::new(Duration::from_millis(millis))
68    }
69
70    /// The limit.
71    pub fn limit(&self) -> Duration {
72        self.limit
73    }
74}
75
76#[async_trait::async_trait]
77impl Middleware for Timeout {
78    async fn handle(&self, request: Request, next: Next) -> Response {
79        // Kept for the log line: `request` is moved into the future, and the
80        // whole point is that the future may not come back.
81        let method = request.method().to_string();
82        let uri = request.uri().path().to_string();
83
84        match tokio::time::timeout(self.limit, next.run(request)).await {
85            Ok(response) => response,
86            Err(_) => {
87                // At `warn`, not `error`: one slow request is a fact about the
88                // request. It is worth seeing, and it is not a fault of the
89                // process.
90                tracing::warn!(
91                    %method,
92                    %uri,
93                    timeout_ms = self.limit.as_millis() as u64,
94                    "request cancelled at its timeout"
95                );
96
97                Error::request_timeout(format!(
98                    "The request did not finish within {} seconds.",
99                    self.limit.as_secs_f64()
100                ))
101                .into_response()
102            }
103        }
104    }
105
106    fn name(&self) -> &'static str {
107        "Timeout"
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::pipeline::Pipeline;
115    use rainier_http::{Method, StatusCode};
116    use std::sync::atomic::{AtomicBool, Ordering};
117    use std::sync::Arc;
118
119    fn request() -> Request {
120        Request::builder().method(Method::GET).uri("/slow").build()
121    }
122
123    #[tokio::test]
124    async fn a_handler_that_finishes_in_time_is_untouched() {
125        let response = Pipeline::new()
126            .through(Timeout::seconds(30))
127            .then(|_| async { Response::ok("in time") })
128            .run(request())
129            .await;
130
131        assert_eq!(response.status(), StatusCode::OK);
132    }
133
134    #[tokio::test]
135    async fn a_handler_that_overruns_gets_408() {
136        let response = Pipeline::new()
137            .through(Timeout::millis(10))
138            .then(|_| async {
139                tokio::time::sleep(Duration::from_secs(30)).await;
140                Response::ok("far too late")
141            })
142            .run(request())
143            .await;
144
145        assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);
146    }
147
148    #[tokio::test]
149    async fn the_overrunning_handler_is_actually_dropped() {
150        // Not just abandoned to run in the background — the whole point is
151        // that it stops holding its connection and its buffers.
152        let finished = Arc::new(AtomicBool::new(false));
153        let flag = Arc::clone(&finished);
154
155        let response = Pipeline::new()
156            .through(Timeout::millis(10))
157            .then(move |_| {
158                let flag = Arc::clone(&flag);
159                async move {
160                    tokio::time::sleep(Duration::from_millis(500)).await;
161                    flag.store(true, Ordering::SeqCst);
162                    Response::ok("never")
163                }
164            })
165            .run(request())
166            .await;
167
168        assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);
169
170        // Long enough that it would have finished had it survived.
171        tokio::time::sleep(Duration::from_millis(700)).await;
172        assert!(!finished.load(Ordering::SeqCst), "the handler kept running past its timeout");
173    }
174
175    #[tokio::test]
176    async fn the_message_says_how_long_it_waited() {
177        let response = Pipeline::new()
178            .through(Timeout::millis(10))
179            .then(|_| async {
180                tokio::time::sleep(Duration::from_secs(30)).await;
181                Response::ok("no")
182            })
183            .run(request())
184            .await;
185
186        let body = response.into_string().await.unwrap();
187        assert!(body.contains("0.01"), "{body}");
188    }
189}