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}