Skip to main content

klauthed_core/time/
clock.rs

1//! The injectable [`Clock`] and its system / fixed implementations.
2
3use std::sync::Mutex;
4
5use time::{Duration, OffsetDateTime};
6
7use super::Timestamp;
8
9/// A source of the current time.
10///
11/// Implementors are `Send + Sync` so a clock can be shared as `Arc<dyn Clock>`
12/// across tasks.
13pub trait Clock: Send + Sync {
14    /// The current instant.
15    fn now(&self) -> Timestamp;
16
17    /// The current instant as a [`time::OffsetDateTime`] (always UTC).
18    fn now_datetime(&self) -> OffsetDateTime {
19        self.now().into_offset_datetime()
20    }
21}
22
23/// The real, system-backed clock for production use.
24#[derive(Debug, Clone, Copy, Default)]
25pub struct SystemClock;
26
27impl Clock for SystemClock {
28    fn now(&self) -> Timestamp {
29        Timestamp::now()
30    }
31}
32
33/// A controllable clock for tests: pin time to a fixed instant and advance it
34/// explicitly. Shareable through `&self`, so it works behind `Arc<dyn Clock>`.
35#[derive(Debug)]
36pub struct FixedClock {
37    now: Mutex<Timestamp>,
38}
39
40impl FixedClock {
41    /// A clock pinned to `at`.
42    pub fn new(at: Timestamp) -> Self {
43        Self { now: Mutex::new(at) }
44    }
45
46    /// A clock pinned to `millis` since the Unix epoch.
47    pub fn at_unix_millis(millis: i64) -> Self {
48        Self::new(Timestamp::from_unix_millis(millis))
49    }
50
51    /// Reset the clock to `at`.
52    pub fn set(&self, at: Timestamp) {
53        *self.now.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = at;
54    }
55
56    /// Move the clock forward (or backward, for a negative delta) by `delta`.
57    #[allow(
58        clippy::expect_used,
59        reason = "advancing this fixed test clock past the representable range is a caller error"
60    )]
61    pub fn advance(&self, delta: Duration) {
62        let mut guard = self.now.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
63        *guard =
64            guard.checked_add(delta).expect("clock advance overflowed the representable range");
65    }
66}
67
68impl Clock for FixedClock {
69    fn now(&self) -> Timestamp {
70        *self.now.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
71    }
72}