autumn-web 0.9.0

An opinionated, convention-over-configuration web framework for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Request deadlines (issue #3058).
//!
//! The inbound request timeout sets a [`Deadline`] for the handler task.
//! Framework calls read it with [`Deadline::current`]:
//!
//! - The outbound HTTP client limits each attempt to the time left. It does
//!   not start a retry that has no time left.
//! - The client sends the time left downstream in [`DEADLINE_HEADER`].
//! - `Db` limits the connection wait to the time left.
//!
//! A task that you start with `tokio::spawn` does not get the deadline. Use
//! [`Deadline::scope`] to give it one. Use [`bounded`] to limit other calls,
//! for example Redis calls.
//!
//! ```rust,no_run
//! use autumn_web::deadline::Deadline;
//!
//! # async fn demo() {
//! if let Some(deadline) = Deadline::current() {
//!     tracing::info!(remaining_ms = deadline.remaining().as_millis(), "time left");
//! }
//! # }
//! ```

// autumn-determinism-gate: read time only through `tokio::time::Instant`, so
// a paused runtime and `#[sim_test]` control it. See CONTRIBUTING.md
// "Determinism seam gate" (issue #1797).
#![cfg_attr(not(test), deny(clippy::disallowed_methods))]

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;

use tokio::time::Instant;

/// The header that carries the time left, in whole milliseconds.
///
/// The value is relative, not a timestamp, so clock skew has no effect. The
/// outbound client sends it when a deadline is set. The server reads it only
/// when `server.timeouts.accept_deadline_header` is `true`, and it can only
/// make the route deadline shorter.
pub const DEADLINE_HEADER: &str = "x-autumn-deadline-ms";

tokio::task_local! {
    static CURRENT: Option<Deadline>;
}

/// A point in time after which the request has no value.
///
/// Uses [`tokio::time::Instant`], so a paused runtime and `#[sim_test]`
/// control it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Deadline {
    at: Instant,
}

impl Deadline {
    /// A deadline `timeout` from now. A very large `timeout` is capped.
    #[must_use]
    pub fn after(timeout: Duration) -> Self {
        Self {
            at: crate::time_math::saturating_tokio_deadline(Instant::now(), timeout),
        }
    }

    /// A deadline at `at`.
    #[must_use]
    pub const fn at(at: Instant) -> Self {
        Self { at }
    }

    /// The deadline of the current task, if one is set.
    #[must_use]
    pub fn current() -> Option<Self> {
        CURRENT.try_with(|deadline| *deadline).ok().flatten()
    }

    /// The instant of the deadline.
    #[must_use]
    pub const fn instant(self) -> Instant {
        self.at
    }

    /// The time left. Zero when the deadline has passed.
    #[must_use]
    pub fn remaining(self) -> Duration {
        self.at.saturating_duration_since(Instant::now())
    }

    /// `true` when no time is left.
    #[must_use]
    pub fn is_expired(self) -> bool {
        self.remaining().is_zero()
    }

    /// `limit`, or the time left if that is shorter.
    #[must_use]
    pub fn clamp(self, limit: Duration) -> Duration {
        limit.min(self.remaining())
    }

    /// Run `future` with this deadline as [`current`](Self::current).
    ///
    /// If a deadline is set already, the earlier one applies. This does not
    /// stop `future` at the deadline. Use [`bounded`] for that.
    pub async fn scope<F: Future>(self, future: F) -> F::Output {
        self.scope_future(future).await
    }

    /// Like [`scope`](Self::scope), but returns a named future type.
    ///
    /// The enclosing deadline is read at each poll, not here, so a future
    /// built outside a scope and polled inside it keeps the earlier one.
    pub const fn scope_future<F: Future>(self, future: F) -> DeadlineScope<F> {
        DeadlineScope {
            deadline: self,
            future,
        }
    }

    /// Like [`scope`](Self::scope), for code that is not async.
    pub fn sync_scope<R>(self, f: impl FnOnce() -> R) -> R {
        CURRENT.sync_scope(Some(self.nested()), f)
    }

    /// This deadline, or the current one when that is earlier.
    fn nested(self) -> Self {
        Self::current().map_or(self, |current| current.min(self))
    }
}

/// Parse a [`DEADLINE_HEADER`] value. `None` when it is not a whole number
/// of milliseconds.
#[must_use]
pub fn parse_header(value: &http::HeaderValue) -> Option<Duration> {
    let text = value.to_str().ok()?.trim();
    if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) {
        return None;
    }
    text.parse().ok().map(Duration::from_millis)
}

pin_project_lite::pin_project! {
    /// The future of [`Deadline::scope_future`]. Each poll runs the inner
    /// future with its deadline, or the enclosing one when that is earlier.
    pub struct DeadlineScope<F> {
        deadline: Deadline,
        #[pin]
        future: F,
    }
}

impl<F: Future> Future for DeadlineScope<F> {
    type Output = F::Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
        let this = self.project();
        let deadline = this.deadline.nested();
        CURRENT.sync_scope(Some(deadline), || this.future.poll(cx))
    }
}

/// Run `future` with no deadline, as a separate process would. A simulated
/// host uses it: only the [`DEADLINE_HEADER`] carries the deadline to it.
#[cfg(feature = "http-client")]
pub(crate) async fn unscoped<F: Future>(future: F) -> F::Output {
    CURRENT.scope(None, future).await
}

/// The error of [`bounded`]: the deadline passed first.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("deadline exceeded")]
pub struct DeadlineExceeded;

/// A framework call the request deadline stopped, reported with its own
/// message and status (for example the `503` of a database connection wait).
/// Like [`DeadlineExceeded`], its response tells the session layer not to
/// save partial session changes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("{0}")]
pub(crate) struct DeadlineStopped(pub(crate) &'static str);

/// Run `future`, but stop it at the current deadline.
///
/// With no deadline set, this runs `future` to the end. Past the deadline,
/// `future` is not polled again.
///
/// # Errors
///
/// [`DeadlineExceeded`] when the deadline passes first.
pub async fn bounded<F: Future>(future: F) -> Result<F::Output, DeadlineExceeded> {
    match Deadline::current() {
        Some(deadline) => Bounded::until(deadline, future).await,
        None => Ok(future.await),
    }
}

pin_project_lite::pin_project! {
    /// The future of [`bounded`] under a deadline, also used by the request
    /// timeout layer.
    ///
    /// Unlike `tokio::time::timeout_at`, which polls the inner future before
    /// it checks the timer, this checks the deadline before every poll, so
    /// work that is ready only at or after the deadline does not run.
    ///
    /// Each poll also runs the inner future with the deadline as
    /// [`Deadline::current`], or the enclosing one when that is earlier, as
    /// [`DeadlineScope`] does. An earlier enclosing deadline also stops it. The deadline is kept once, in the timer: the
    /// request timeout layer holds this future in every request, and the
    /// allocation gate (`tests/config_alloc_gate.rs`) counts its size.
    pub(crate) struct Bounded<F> {
        #[pin]
        sleep: tokio::time::Sleep,
        #[pin]
        future: F,
    }
}

impl<F> Bounded<F> {
    /// Run `future` until `deadline`.
    pub(crate) fn until(deadline: Deadline, future: F) -> Self {
        Self {
            sleep: tokio::time::sleep_until(deadline.instant()),
            future,
        }
    }
}

impl<F: Future> Future for Bounded<F> {
    type Output = Result<F::Output, DeadlineExceeded>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let mut this = self.project();
        // An enclosing deadline that is earlier applies: check it, and move
        // the timer to it, so the task wakes then.
        let deadline = Deadline::at(this.sleep.deadline()).nested();
        if deadline.instant() < this.sleep.deadline() {
            this.sleep.as_mut().reset(deadline.instant());
        }
        if deadline.is_expired() {
            return Poll::Ready(Err(DeadlineExceeded));
        }
        let future = this.future;
        if let Poll::Ready(output) = CURRENT.sync_scope(Some(deadline), || future.poll(cx)) {
            return Poll::Ready(Ok(output));
        }
        // Wakes the task at the deadline.
        this.sleep.poll(cx).map(|()| Err(DeadlineExceeded))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test(start_paused = true)]
    async fn bounded_stops_at_an_earlier_enclosing_deadline() {
        let ran = std::sync::atomic::AtomicBool::new(false);
        let start = Instant::now();
        // The bound is 5 s away, but the enclosing scope ends at 1 s.
        let bound = Bounded::until(Deadline::after(Duration::from_secs(5)), async {
            tokio::time::sleep(Duration::from_secs(2)).await;
            ran.store(true, std::sync::atomic::Ordering::SeqCst);
        });
        let result = Deadline::after(Duration::from_secs(1)).scope(bound).await;
        assert_eq!(result, Err(DeadlineExceeded));
        assert_eq!(start.elapsed(), Duration::from_secs(1));
        assert!(!ran.load(std::sync::atomic::Ordering::SeqCst));
    }

    #[tokio::test(start_paused = true)]
    async fn bounded_does_not_poll_work_past_the_deadline() {
        let ran = std::sync::atomic::AtomicBool::new(false);
        let deadline = Deadline::after(Duration::from_millis(1));
        tokio::time::advance(Duration::from_millis(5)).await;
        let result = deadline
            .scope(bounded(async {
                ran.store(true, std::sync::atomic::Ordering::SeqCst);
            }))
            .await;
        assert_eq!(result, Err(DeadlineExceeded));
        assert!(!ran.load(std::sync::atomic::Ordering::SeqCst));
    }

    #[tokio::test(start_paused = true)]
    async fn bounded_does_not_resume_work_at_the_deadline() {
        let ran = std::sync::atomic::AtomicBool::new(false);
        let deadline = Deadline::after(Duration::from_millis(10));
        // Live at the first poll; ready again only when the deadline is due.
        let result = deadline
            .scope(bounded(async {
                tokio::time::sleep_until(deadline.instant()).await;
                ran.store(true, std::sync::atomic::Ordering::SeqCst);
            }))
            .await;
        assert_eq!(result, Err(DeadlineExceeded));
        assert!(!ran.load(std::sync::atomic::Ordering::SeqCst));
    }

    #[tokio::test(start_paused = true)]
    async fn remaining_counts_down_to_zero() {
        let deadline = Deadline::after(Duration::from_secs(5));
        assert_eq!(deadline.remaining(), Duration::from_secs(5));
        assert!(!deadline.is_expired());
        tokio::time::advance(Duration::from_secs(2)).await;
        assert_eq!(deadline.remaining(), Duration::from_secs(3));
        tokio::time::advance(Duration::from_secs(4)).await;
        assert_eq!(deadline.remaining(), Duration::ZERO);
        assert!(deadline.is_expired());
    }

    #[tokio::test(start_paused = true)]
    async fn a_scope_built_outside_keeps_an_earlier_outer_deadline() {
        let outer = Deadline::after(Duration::from_secs(1));
        let later = Deadline::after(Duration::from_secs(5));
        let inner = later.scope_future(async { Deadline::current() });
        assert_eq!(outer.scope(inner).await, Some(outer), "the earlier one");

        let inner = outer.scope_future(async { Deadline::current() });
        assert_eq!(
            later.scope(inner).await,
            Some(outer),
            "the inner one is earlier"
        );
    }

    #[tokio::test(start_paused = true)]
    async fn a_huge_timeout_does_not_overflow() {
        let deadline = Deadline::after(Duration::MAX);
        assert!(deadline.remaining() > Duration::from_secs(60 * 60 * 24 * 365));
    }

    #[tokio::test(start_paused = true)]
    async fn clamp_and_min_take_the_shorter_value() {
        let deadline = Deadline::after(Duration::from_secs(5));
        assert_eq!(
            deadline.clamp(Duration::from_secs(30)),
            Duration::from_secs(5)
        );
        assert_eq!(
            deadline.clamp(Duration::from_secs(1)),
            Duration::from_secs(1)
        );
        let earlier = Deadline::after(Duration::from_secs(1));
        assert_eq!(deadline.min(earlier), earlier);
        assert_eq!(earlier.min(deadline), earlier);
    }

    #[tokio::test(start_paused = true)]
    async fn current_is_set_only_inside_a_scope() {
        assert_eq!(Deadline::current(), None);
        let deadline = Deadline::after(Duration::from_secs(5));
        let seen = deadline.scope(async { Deadline::current() }).await;
        assert_eq!(seen, Some(deadline));
        assert_eq!(deadline.sync_scope(Deadline::current), Some(deadline));
        assert_eq!(Deadline::current(), None);
    }

    #[tokio::test(start_paused = true)]
    async fn a_nested_scope_keeps_the_earlier_deadline() {
        let outer = Deadline::after(Duration::from_secs(1));
        let inner = Deadline::after(Duration::from_secs(5));
        let seen = outer
            .scope(async { inner.scope(async { Deadline::current() }).await })
            .await;
        assert_eq!(seen, Some(outer), "a later deadline cannot extend");
        let seen = inner
            .scope(async { outer.scope(async { Deadline::current() }).await })
            .await;
        assert_eq!(seen, Some(outer), "an earlier deadline shortens");
    }

    #[tokio::test(start_paused = true)]
    async fn bounded_stops_at_the_deadline() {
        let deadline = Deadline::after(Duration::from_secs(1));
        let start = Instant::now();
        let outcome = deadline
            .scope(bounded(tokio::time::sleep(Duration::from_secs(10))))
            .await;
        assert_eq!(outcome, Err(DeadlineExceeded));
        assert_eq!(start.elapsed(), Duration::from_secs(1));
        assert_eq!(bounded(async { 7 }).await, Ok(7), "no deadline, no limit");
    }

    #[tokio::test(start_paused = true)]
    async fn unscoped_clears_the_deadline() {
        let deadline = Deadline::after(Duration::from_secs(1));
        let seen = deadline
            .scope(unscoped(async { Deadline::current() }))
            .await;
        assert_eq!(seen, None);
    }

    #[test]
    fn header_values_parse_as_whole_milliseconds() {
        let parse = |raw: &str| parse_header(&http::HeaderValue::from_str(raw).unwrap());
        assert_eq!(parse("1500"), Some(Duration::from_millis(1500)));
        assert_eq!(parse("0"), Some(Duration::ZERO));
        assert_eq!(parse(" 20 "), Some(Duration::from_millis(20)));
        assert_eq!(parse("-1"), None);
        assert_eq!(parse("1.5"), None);
        assert_eq!(parse("soon"), None);
        assert_eq!(parse(""), None);
        assert_eq!(parse("99999999999999999999999"), None);
    }
}