turnframe_store/memory/clock.rs
1//! The clock a store stamps its own columns with.
2//!
3//! A store owns a few timestamps the caller does not supply: when a phase
4//! marker was last written, when a journal outcome was recorded, when a commit
5//! landed. Reading the wall clock for those makes tests that involve expiry or
6//! ordering flaky, so the in-memory store takes its time from a [`Clock`].
7//!
8//! Use [`SystemClock`] in production and [`ManualClock`] in tests: nothing
9//! moves until the test moves it, so "the card expired" is an assertion rather
10//! than a sleep.
11
12use std::fmt;
13use std::sync::Mutex;
14
15use chrono::{DateTime, TimeDelta, Utc};
16
17/// Source of the current time for a store.
18///
19/// Implementations must be cheap and must never block: a store calls
20/// [`Clock::now`] while it holds its internal lock.
21pub trait Clock: Send + Sync + fmt::Debug {
22 /// The current instant, in UTC.
23 fn now(&self) -> DateTime<Utc>;
24}
25
26/// The wall clock.
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
28pub struct SystemClock;
29
30impl Clock for SystemClock {
31 fn now(&self) -> DateTime<Utc> {
32 Utc::now()
33 }
34}
35
36/// A clock that only moves when a test moves it.
37///
38/// ```rust
39/// use chrono::{TimeDelta, Utc};
40/// use turnframe_store::memory::{Clock, ManualClock};
41///
42/// let clock = ManualClock::new(Utc::now());
43/// let start = clock.now();
44/// clock.advance(TimeDelta::seconds(30));
45/// assert_eq!(clock.now(), start + TimeDelta::seconds(30));
46/// ```
47#[derive(Debug)]
48pub struct ManualClock {
49 now: Mutex<DateTime<Utc>>,
50}
51
52impl ManualClock {
53 /// A clock stopped at `start`.
54 #[must_use]
55 pub fn new(start: DateTime<Utc>) -> Self {
56 Self {
57 now: Mutex::new(start),
58 }
59 }
60
61 /// A clock stopped at the Unix epoch, for tests that only care about order.
62 #[must_use]
63 pub fn epoch() -> Self {
64 Self::new(DateTime::<Utc>::UNIX_EPOCH)
65 }
66
67 /// Moves the clock to `instant`, forwards or backwards.
68 ///
69 /// A poisoned lock is ignored: the clock is a test fixture and losing its
70 /// value cannot corrupt anything a store relies on.
71 pub fn set(&self, instant: DateTime<Utc>) {
72 match self.now.lock() {
73 Ok(mut guard) => *guard = instant,
74 Err(poisoned) => *poisoned.into_inner() = instant,
75 }
76 }
77
78 /// Moves the clock forward by `delta`.
79 pub fn advance(&self, delta: TimeDelta) {
80 let next = self.now() + delta;
81 self.set(next);
82 }
83}
84
85impl Default for ManualClock {
86 fn default() -> Self {
87 Self::epoch()
88 }
89}
90
91impl Clock for ManualClock {
92 fn now(&self) -> DateTime<Utc> {
93 match self.now.lock() {
94 Ok(guard) => *guard,
95 Err(poisoned) => *poisoned.into_inner(),
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn manual_clock_is_stopped_until_moved() {
106 let clock = ManualClock::epoch();
107 assert_eq!(clock.now(), DateTime::<Utc>::UNIX_EPOCH);
108 assert_eq!(clock.now(), clock.now());
109 clock.advance(TimeDelta::hours(2));
110 assert_eq!(
111 clock.now(),
112 DateTime::<Utc>::UNIX_EPOCH + TimeDelta::hours(2)
113 );
114 clock.set(DateTime::<Utc>::UNIX_EPOCH);
115 assert_eq!(clock.now(), DateTime::<Utc>::UNIX_EPOCH);
116 }
117
118 #[test]
119 fn system_clock_moves_forward() {
120 let clock = SystemClock;
121 assert!(clock.now() <= clock.now());
122 }
123}