Skip to main content

polyoxide_rt/
time.rs

1//! Timeouts and intervals, on an injected `Runtime`.
2
3use std::future::{poll_fn, Future};
4use std::pin::pin;
5use std::sync::Arc;
6use std::task::Poll;
7use std::time::Duration;
8
9use crate::{Instant, Runtime};
10
11/// A tick this close to its deadline is on time, so scheduling jitter does not
12/// shift the grid of `MissedTick::Delay`; tokio's interval makes the same
13/// allowance.
14const TOLERANCE: Duration = Duration::from_millis(5);
15
16/// What `instant` plus an unrepresentably long wait stands for: thirty years.
17const FAR: Duration = Duration::from_secs(86_400 * 365 * 30);
18
19/// Await `future` for at most `duration` on `runtime`'s clock: its output, or
20/// `None` when `duration` passed first.
21///
22/// The duration counts from the first poll, not from the call. `future` is
23/// polled before the timer, so a ready future wins even at `Duration::ZERO`. Any
24/// duration, `Duration::MAX` included, is valid.
25pub async fn timeout<F: Future>(
26    runtime: &dyn Runtime,
27    duration: Duration,
28    future: F,
29) -> Option<F::Output> {
30    let mut future = pin!(future);
31    let mut deadline = runtime.sleep(duration);
32    poll_fn(|cx| {
33        if let Poll::Ready(output) = future.as_mut().poll(cx) {
34            return Poll::Ready(Some(output));
35        }
36        deadline.as_mut().poll(cx).map(|()| None)
37    })
38    .await
39}
40
41/// What an [`Interval`] does about ticks it was too late to take on time.
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43#[non_exhaustive]
44pub enum MissedTick {
45    /// Fire the missed ticks one after another at once, then go back to the
46    /// original grid.
47    #[default]
48    Burst,
49    /// Start the next period when the late tick fires, so the grid moves.
50    Delay,
51    /// Drop the missed ticks and fire the next one on the original grid.
52    Skip,
53}
54
55/// Ticks every `period`, the first at once, on `runtime`'s clock.
56///
57/// # Panics
58///
59/// Panics with `interval period must be non-zero` when `period` is zero.
60pub fn interval(runtime: Arc<dyn Runtime>, period: Duration) -> Interval {
61    assert!(!period.is_zero(), "interval period must be non-zero");
62    let next = runtime.now();
63    Interval {
64        runtime,
65        period,
66        next,
67        behaviour: MissedTick::default(),
68    }
69}
70
71/// A stream of ticks a period apart; built by [`interval`].
72///
73/// It is built on the runtime's `sleep_until` and `now`, so a runtime's paused
74/// clock drives it.
75#[derive(Debug)]
76pub struct Interval {
77    runtime: Arc<dyn Runtime>,
78    period: Duration,
79    next: Instant,
80    behaviour: MissedTick,
81}
82
83impl Interval {
84    /// Wait for the next tick and return the deadline it was due at. Dropping
85    /// the future before it completes loses nothing: the next call waits for
86    /// the same tick.
87    pub async fn tick(&mut self) -> Instant {
88        let due = self.next;
89        self.runtime.sleep_until(due).await;
90        self.next = self.after(due, self.runtime.now());
91        due
92    }
93
94    /// Start a new period now: the next tick is one period from this call.
95    pub fn reset(&mut self) {
96        self.next = later(self.runtime.now(), self.period);
97    }
98
99    /// The time between ticks.
100    pub fn period(&self) -> Duration {
101        self.period
102    }
103
104    /// Choose what to do about ticks that were missed.
105    pub fn set_missed_tick(&mut self, behaviour: MissedTick) {
106        self.behaviour = behaviour;
107    }
108
109    /// The deadline after the tick due at `due`, taken at `now`.
110    fn after(&self, due: Instant, now: Instant) -> Instant {
111        let next = later(due, self.period);
112        if now.saturating_duration_since(due) <= TOLERANCE {
113            return next;
114        }
115        match self.behaviour {
116            MissedTick::Burst => next,
117            MissedTick::Delay => later(now, self.period),
118            MissedTick::Skip => {
119                if next > now {
120                    return next;
121                }
122                let behind = now.saturating_duration_since(due).as_nanos();
123                let periods = behind / self.period.as_nanos();
124                u32::try_from(periods + 1)
125                    .ok()
126                    .and_then(|whole| self.period.checked_mul(whole))
127                    .and_then(|grid| due.checked_add(grid))
128                    .unwrap_or_else(|| later(now, self.period))
129            }
130        }
131    }
132}
133
134/// `instant` plus `wait`, capped at thirty years where the sum would overflow.
135fn later(instant: Instant, wait: Duration) -> Instant {
136    instant.checked_add(wait).unwrap_or_else(|| instant + FAR)
137}