Skip to main content

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}