Skip to main content

macrame/util/
clock.rs

1use crate::error::Result;
2use crate::util::timestamp;
3use std::sync::Mutex;
4use std::time::SystemTime;
5
6/// Trait defining the clock interface for timestamp generation.
7/// CONTRACT: successive calls return strictly increasing values,
8/// even across application restarts and NTP corrections.
9pub trait Clock: Send + Sync {
10    /// Returns the current timestamp as ISO-8601 UTC string (e.g. "YYYY-MM-DDTHH:MM:SS.ffffffZ").
11    fn now(&self) -> String;
12
13    /// Raise this clock's floor to `floor`, if it is currently behind it.
14    ///
15    /// **The contract above is what makes this part of the trait rather than an
16    /// implementation detail of [`SystemClock`].** "Strictly increasing across
17    /// restarts" is not a property a clock can hold on its own — it depends on
18    /// what the database already contains, which the clock cannot see. Every
19    /// implementation therefore needs a way to be told, and
20    /// [`crate::Database::open_with_clock`] tells it exactly once, at open.
21    ///
22    /// Without this, injecting a [`FakeClock`] was not merely awkward but
23    /// unusable on any database with rows in it: a fake starting at the epoch
24    /// issues stamps below every existing `recorded_at` and the first concept
25    /// write aborts on `trg_concepts_monotonic_ra`. That is the reason defect K
26    /// stalled for three releases, and it is a missing trait method rather than
27    /// a missing constructor argument.
28    fn raise_floor(&self, floor: SystemTime);
29}
30
31/// The newest `recorded_at` in the ledger tables, or `None` on an empty database.
32///
33/// One definition of "the floor", used by [`SystemClock::new`] and by
34/// `open_with_clock`, rather than the query existing twice and being kept in
35/// step by hand.
36pub(crate) async fn recorded_at_floor(conn: &libsql::Connection) -> Result<Option<SystemTime>> {
37    let max_ts: Option<String> = conn
38        .query(
39            "SELECT MAX(recorded_at) FROM (
40                 SELECT MAX(recorded_at) AS recorded_at FROM concepts
41                 UNION ALL
42                 SELECT MAX(recorded_at) AS recorded_at FROM links
43             )",
44            (),
45        )
46        .await?
47        .next()
48        .await?
49        .and_then(|row| row.get(0).ok());
50
51    Ok(match max_ts {
52        Some(ts) => match parse_iso8601_utc(&ts) {
53            Ok(t) => Some(t),
54            Err(e) => {
55                tracing::warn!(
56                    "clock: failed to parse MAX(recorded_at)={:?}: {}; no floor applied",
57                    ts,
58                    e
59                );
60                None
61            }
62        },
63        None => None,
64    })
65}
66
67/// Production system clock maintaining a monotonic timestamp floor.
68pub struct SystemClock {
69    last_issued: Mutex<SystemTime>,
70}
71
72impl SystemClock {
73    pub async fn new(conn: &libsql::Connection) -> Result<Self> {
74        // Wall clock when the database is empty or its stamp will not parse:
75        // there is nothing to be behind, and a corrupt stored timestamp must not
76        // become this process's floor (D-027).
77        let floor = recorded_at_floor(conn)
78            .await?
79            .unwrap_or_else(SystemTime::now);
80        Ok(Self {
81            last_issued: Mutex::new(floor),
82        })
83    }
84}
85
86impl Clock for SystemClock {
87    fn now(&self) -> String {
88        let mut guard = self.last_issued.lock().unwrap();
89        let wall = SystemTime::now();
90        let next = if wall > *guard {
91            wall
92        } else {
93            *guard + std::time::Duration::from_micros(1)
94        };
95        *guard = next;
96        format_iso8601_utc(next)
97    }
98
99    fn raise_floor(&self, floor: SystemTime) {
100        let mut guard = self.last_issued.lock().unwrap();
101        if floor > *guard {
102            *guard = floor;
103        }
104    }
105}
106
107/// Fake clock for deterministic unit and scenario testing.
108///
109/// Inject with [`crate::Database::open_with_clock`]. On a **fresh** database the
110/// stamps are exactly what the caller sets, which is the point: a bitemporal
111/// test that has to accommodate wall-clock `recorded_at` values cannot assert on
112/// them, so most of the interesting divergence properties were previously
113/// written against `valid_time` alone or against a hand-driven connection.
114///
115/// On a database that **already holds rows**, `open_with_clock` raises this
116/// clock to their newest `recorded_at` before the actor starts — see
117/// [`Clock::raise_floor`]. That is not optional and it does cost determinism:
118/// reopening a populated database with a fake starting at the epoch would
119/// otherwise abort the first concept write on `trg_concepts_monotonic_ra`. Tests
120/// wanting exact stamps should start from an empty file.
121pub struct FakeClock {
122    current: Mutex<SystemTime>,
123}
124
125impl FakeClock {
126    pub fn new(initial: SystemTime) -> Self {
127        Self {
128            current: Mutex::new(initial),
129        }
130    }
131
132    pub fn advance(&self, duration: std::time::Duration) {
133        let mut guard = self.current.lock().unwrap();
134        *guard += duration;
135    }
136
137    /// The stamp this clock would issue next, without issuing it.
138    pub fn peek(&self) -> String {
139        format_iso8601_utc(*self.current.lock().unwrap())
140    }
141}
142
143impl Clock for FakeClock {
144    fn now(&self) -> String {
145        let mut guard = self.current.lock().unwrap();
146        let res = format_iso8601_utc(*guard);
147        *guard += std::time::Duration::from_micros(1);
148        res
149    }
150
151    fn raise_floor(&self, floor: SystemTime) {
152        let mut guard = self.current.lock().unwrap();
153        if floor > *guard {
154            // One microsecond past, not equal to: `recorded_at` must be
155            // *strictly* increasing, and issuing the floor itself would collide
156            // with the row it was read from.
157            *guard = floor + std::time::Duration::from_micros(1);
158        }
159    }
160}
161
162/// Strict parser for the canonical timestamp form (§4.1), accepting the legacy
163/// second-precision form for rows written by older crate versions.
164///
165/// Both this and [`format_iso8601_utc`] delegate to [`crate::util::timestamp`],
166/// which owns the canonical form. They remain here as the names §5.1.1 uses.
167pub fn parse_iso8601_utc(s: &str) -> Result<SystemTime> {
168    timestamp::parse(s)
169}
170
171/// Format a `SystemTime` in the canonical form `YYYY-MM-DDTHH:MM:SS.ffffffZ`.
172pub fn format_iso8601_utc(st: SystemTime) -> String {
173    timestamp::format(st)
174}