Skip to main content

onetaskgraph_plugin_api/
clock.rs

1//! The clock a source paces and backs off on.
2//!
3//! A plugin that waits — between pages to stay under a published rate, or before retrying a
4//! refused request — waits on the [`Clock`] it was built with rather than on the runtime's own
5//! timer, so a test can run it on simulated time: a minute of pacing then costs no real time,
6//! and the figures a timing budget reports are the same on every run. The binary hands every
7//! in-process source the process's one clock through [`SourcePlugin::build_with_clock`]; with
8//! nothing asking for simulated time that is [`system_clock`], and every plugin behaves exactly
9//! as it does on the runtime's timer.
10//!
11//! [`SourcePlugin::build_with_clock`]: crate::SourcePlugin::build_with_clock
12
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17
18/// A source of monotonic time and of waits measured against it.
19pub trait Clock: Send + Sync {
20    /// Monotonic time elapsed since this clock's origin.
21    fn now(&self) -> std::time::Duration;
22    /// Complete once `duration` has passed on this clock.
23    fn sleep(
24        &self,
25        duration: std::time::Duration,
26    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>>;
27    /// [`sleep`](Self::sleep), for a wait whose reason is `cause` — the fixed word a plugin
28    /// names its waits by, such as the limit it is pacing under.
29    ///
30    /// A clock that keeps a record of what waited and why — a test's simulated one — keeps the
31    /// cause beside the wait; every other clock waits exactly as `sleep` does, which is what
32    /// this is defaulted to.
33    fn sleep_because(
34        &self,
35        duration: std::time::Duration,
36        cause: WaitCause,
37    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>> {
38        let _ = cause;
39        self.sleep(duration)
40    }
41}
42
43/// The word a plugin names one kind of wait by — see [`Clock::sleep_because`].
44///
45/// One word: a nonempty run of printable ASCII with no whitespace, because a simulated clock
46/// carries it as one word of a line. [`WaitCause::new`] refuses anything else, and it is a
47/// `const fn`, so a cause declared as a constant that is not a word fails the build rather
48/// than reaching a clock.
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub struct WaitCause(&'static str);
51
52impl WaitCause {
53    /// The cause named `word`.
54    ///
55    /// # Panics
56    ///
57    /// When `word` is not [one word](Self::is_word) — at compile time, for a constant.
58    #[must_use]
59    pub const fn new(word: &'static str) -> Self {
60        assert!(
61            Self::is_word(word),
62            "a wait cause is one word of printable ASCII"
63        );
64        Self(word)
65    }
66
67    /// Whether `text` is a word a cause may be: nonempty printable ASCII, no whitespace.
68    ///
69    /// The one statement of the rule, for whatever reads a cause back off a clock's wire.
70    #[must_use]
71    pub const fn is_word(text: &str) -> bool {
72        let bytes = text.as_bytes();
73        if bytes.is_empty() {
74            return false;
75        }
76        let mut index = 0;
77        while index < bytes.len() {
78            if !bytes[index].is_ascii_graphic() {
79                return false;
80            }
81            index += 1;
82        }
83        true
84    }
85
86    /// The word itself.
87    #[must_use]
88    pub const fn as_str(self) -> &'static str {
89        self.0
90    }
91}
92
93impl std::fmt::Display for WaitCause {
94    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        formatter.write_str(self.0)
96    }
97}
98
99/// The variable that puts a process of this build on a simulated clock.
100///
101/// Its value is `<address>/<client>`: a test's coordinator's loopback address and the
102/// process's 0-based client number. Unset or empty, the process runs on [`system_clock`]. It
103/// does not begin `ONETASKGRAPH_`, so the configuration's environment layer never reads it as a
104/// setting. Named here, beside the clock it selects, so the binary that reads it and the test
105/// harness that hands it out spell it once between them.
106pub const SIMULATED_CLOCK_VARIABLE: &str = "OTG_SIMULATED_CLOCK";
107
108/// One clock, shared by everything a process builds.
109pub type SharedClock = std::sync::Arc<dyn Clock>;
110
111/// The real clock: `tokio::time::sleep` and a monotonic `Instant`.
112#[must_use]
113pub fn system_clock() -> SharedClock {
114    Arc::new(SystemClock {
115        origin: Instant::now(),
116    })
117}
118
119/// [`system_clock`]'s clock: its origin is the instant it was made.
120struct SystemClock {
121    origin: Instant,
122}
123
124impl Clock for SystemClock {
125    fn now(&self) -> Duration {
126        self.origin.elapsed()
127    }
128
129    fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
130        Box::pin(tokio::time::sleep(duration))
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::WaitCause;
137
138    #[test]
139    fn a_word_of_printable_ascii_is_a_cause_and_nothing_else_is() {
140        const FILING: WaitCause = WaitCause::new("board-filing");
141        assert_eq!(FILING.as_str(), "board-filing");
142        assert_eq!(FILING.to_string(), "board-filing");
143        for refused in [
144            "",
145            " ",
146            "two words",
147            "tab\there",
148            "line\n",
149            "café",
150            "\u{a0}nbsp",
151        ] {
152            assert!(!WaitCause::is_word(refused), "{refused:?} is not one word");
153        }
154    }
155
156    #[test]
157    #[should_panic(expected = "a wait cause is one word of printable ASCII")]
158    fn a_cause_that_is_not_one_word_is_refused_where_it_is_made() {
159        let words = String::from("two words");
160        let _ = WaitCause::new(Box::leak(words.into_boxed_str()));
161    }
162}