Skip to main content

areev_trigger/
clock.rs

1//! The one place this crate reads the wall clock.
2//!
3//! Modelled on `areev_run::Clock`, and for the same reason: a subsystem whose
4//! whole job is time-dependent is untestable if it calls `SystemTime::now()`
5//! inline. Every decision here — is this due, has the lease expired, when is the
6//! next occurrence — takes `now_ms` as a value, so a test can step across a cron
7//! boundary or a lease expiry without sleeping.
8//!
9//! On the embedded backend the host clock *is* the database clock, because
10//! there is exactly one writer on one machine. On Postgres, where several nodes
11//! share a memory, readings should come from the server (`now()`) instead:
12//! skewed node clocks otherwise make triggers fire early, late, or twice, and
13//! that is miserable to debug after the fact.
14
15/// A source of epoch-milliseconds.
16pub trait Clock: Send + Sync {
17    fn now_ms(&self) -> i64;
18}
19
20/// The wall clock, with a simulation override.
21///
22/// `AREEV_TRIGGER_NOW_MS` pins the reading, mirroring the loop's
23/// `AREEV_LOOP_NOW_MS` seam. A set-but-unparseable value **panics**: the caller
24/// explicitly asked for simulated time, and silently running at wall time
25/// instead would defeat the point and make a golden test pass for the wrong
26/// reason.
27pub struct SystemClock;
28
29impl Clock for SystemClock {
30    fn now_ms(&self) -> i64 {
31        if let Ok(v) = std::env::var("AREEV_TRIGGER_NOW_MS") {
32            return v.parse::<i64>().unwrap_or_else(|_| {
33                panic!("AREEV_TRIGGER_NOW_MS is set but not an integer: {v:?}")
34            });
35        }
36        std::time::SystemTime::now()
37            .duration_since(std::time::UNIX_EPOCH)
38            .map(|d| d.as_millis() as i64)
39            .unwrap_or(0)
40    }
41}
42
43/// A clock that returns exactly what a test tells it to.
44pub struct FixedClock(pub std::sync::atomic::AtomicI64);
45
46impl FixedClock {
47    pub fn new(now_ms: i64) -> Self {
48        FixedClock(std::sync::atomic::AtomicI64::new(now_ms))
49    }
50    pub fn set(&self, now_ms: i64) {
51        self.0.store(now_ms, std::sync::atomic::Ordering::SeqCst);
52    }
53    /// Move time forward. Never backward — a clock that can go back would let a
54    /// test pass a case that cannot happen.
55    pub fn advance(&self, ms: i64) {
56        assert!(ms >= 0, "time does not run backwards");
57        self.0.fetch_add(ms, std::sync::atomic::Ordering::SeqCst);
58    }
59}
60
61impl Clock for FixedClock {
62    fn now_ms(&self) -> i64 {
63        self.0.load(std::sync::atomic::Ordering::SeqCst)
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn fixed_clock_reads_and_advances() {
73        let c = FixedClock::new(1_000);
74        assert_eq!(c.now_ms(), 1_000);
75        c.advance(500);
76        assert_eq!(c.now_ms(), 1_500);
77        c.set(42);
78        assert_eq!(c.now_ms(), 42);
79    }
80
81    #[test]
82    #[should_panic(expected = "time does not run backwards")]
83    fn fixed_clock_refuses_to_rewind() {
84        FixedClock::new(10).advance(-1);
85    }
86
87    #[test]
88    fn the_simulation_seam_is_honoured() {
89        // Serialised with the next test by running them in one #[test]: env
90        // vars are process-global and these two would race.
91        std::env::set_var("AREEV_TRIGGER_NOW_MS", "1755600000000");
92        assert_eq!(SystemClock.now_ms(), 1_755_600_000_000);
93        std::env::remove_var("AREEV_TRIGGER_NOW_MS");
94
95        // …and a garbled value must fail loud rather than falling back to wall
96        // time, which would make a time-travel test silently meaningless.
97        std::env::set_var("AREEV_TRIGGER_NOW_MS", "soon");
98        let panicked = std::panic::catch_unwind(|| SystemClock.now_ms()).is_err();
99        std::env::remove_var("AREEV_TRIGGER_NOW_MS");
100        assert!(panicked, "a set-but-unparseable override must panic");
101    }
102}