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}