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}