Skip to main content

kimetsu_core/
clock.rs

1//! v3.0 #3 Slice B: Hybrid Logical Clock (HLC) for convergent team sync.
2//!
3//! An HLC stamps every event with a timestamp that is (a) globally
4//! lexicographically sortable, (b) monotonic on a single brain, and (c) CAUSAL
5//! across brains — receiving a remote event advances the local clock past it, so
6//! any later local event sorts after everything observed. Replaying a merged
7//! event log in HLC order is therefore deterministic on every brain, which makes
8//! the projection converge field-by-field (last-writer-in-HLC-order wins) without
9//! per-field bookkeeping. This generalizes the single-brain `(ts, rowid)` causal
10//! order to the multi-brain case.
11//!
12//! The canonical wire/storage form is `"{wall_ms:013}.{counter:010}.{node}"` —
13//! zero-padded so plain string comparison equals causal comparison. 13 digits
14//! covers epoch-millis through year 5138; 10 digits covers the full u32 counter
15//! range (the counter only grows within a single ms, then resets). The 10-digit
16//! width is also wide enough for the v9 migration to backfill a row's `rowid`
17//! into the counter slot (`wall = 0`), so old events sort before new ones by a
18//! consistent string width.
19
20use std::sync::{Mutex, OnceLock};
21use std::time::{SystemTime, UNIX_EPOCH};
22
23/// A Hybrid Logical Clock timestamp.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Hlc {
26    pub wall_ms: u64,
27    pub counter: u32,
28    pub node: String,
29}
30
31impl Hlc {
32    /// Canonical sortable string: `{wall_ms:013}.{counter:010}.{node}`.
33    pub fn to_canonical(&self) -> String {
34        format!("{:013}.{:010}.{}", self.wall_ms, self.counter, self.node)
35    }
36
37    /// Parse a canonical string back into an `Hlc`. The node may itself contain
38    /// `.` (e.g. `machine.local`), so only the first two `.`-separated fields are
39    /// structured; the remainder is the node.
40    pub fn parse(s: &str) -> Option<Hlc> {
41        let mut it = s.splitn(3, '.');
42        let wall_ms = it.next()?.parse::<u64>().ok()?;
43        let counter = it.next()?.parse::<u32>().ok()?;
44        let node = it.next()?.to_string();
45        Some(Hlc {
46            wall_ms,
47            counter,
48            node,
49        })
50    }
51}
52
53struct State {
54    wall_ms: u64,
55    counter: u32,
56}
57
58fn state() -> &'static Mutex<State> {
59    static STATE: OnceLock<Mutex<State>> = OnceLock::new();
60    STATE.get_or_init(|| {
61        Mutex::new(State {
62            wall_ms: 0,
63            counter: 0,
64        })
65    })
66}
67
68/// Process-global node id (the machine part of the write origin). Defaults to
69/// `"local"` until [`set_node`] is called at startup.
70fn node_cell() -> &'static OnceLock<String> {
71    static NODE: OnceLock<String> = OnceLock::new();
72    &NODE
73}
74
75/// Set this process's HLC node id once at startup (first call wins). Use the
76/// machine part of the write origin so equal `(wall, counter)` ties break by
77/// machine — a globally consistent total order. Empty input is ignored.
78pub fn set_node(node: impl Into<String>) {
79    let n = node.into();
80    if !n.trim().is_empty() {
81        let _ = node_cell().set(n);
82    }
83}
84
85fn node() -> String {
86    node_cell()
87        .get()
88        .cloned()
89        .unwrap_or_else(|| "local".to_string())
90}
91
92fn physical_now_ms() -> u64 {
93    SystemTime::now()
94        .duration_since(UNIX_EPOCH)
95        .map(|d| d.as_millis() as u64)
96        .unwrap_or(0)
97}
98
99/// Generate the next local HLC timestamp (monotonic). Within the same wall
100/// millisecond the counter increments; a newer wall clock resets it to 0.
101pub fn now() -> Hlc {
102    let mut st = state().lock().unwrap_or_else(|p| p.into_inner());
103    let phys = physical_now_ms();
104    if phys > st.wall_ms {
105        st.wall_ms = phys;
106        st.counter = 0;
107    } else {
108        // Same or backwards physical clock → keep the logical wall, bump counter.
109        st.counter = st.counter.saturating_add(1);
110    }
111    Hlc {
112        wall_ms: st.wall_ms,
113        counter: st.counter,
114        node: node(),
115    }
116}
117
118/// Observe a remote HLC (on sync import): advance the local clock past
119/// `max(physical, local, remote)` so every subsequent local event sorts AFTER
120/// everything received — the causality guarantee that makes total-order replay
121/// deterministic across brains.
122pub fn observe(remote: &Hlc) {
123    let mut st = state().lock().unwrap_or_else(|p| p.into_inner());
124    let phys = physical_now_ms();
125    let max_wall = st.wall_ms.max(remote.wall_ms).max(phys);
126    if max_wall == st.wall_ms && max_wall == remote.wall_ms {
127        // All three share a wall ms → counter must exceed both seen counters.
128        st.counter = st.counter.max(remote.counter).saturating_add(1);
129    } else if max_wall == remote.wall_ms {
130        // Remote's wall dominates → adopt it, counter just past the remote's.
131        st.wall_ms = max_wall;
132        st.counter = remote.counter.saturating_add(1);
133    } else if max_wall == st.wall_ms {
134        // Local wall still dominates → keep advancing the local counter.
135        st.counter = st.counter.saturating_add(1);
136    } else {
137        // Physical clock dominates both → fresh tick.
138        st.wall_ms = max_wall;
139        st.counter = 0;
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn now_is_strictly_increasing() {
149        let a = now();
150        let b = now();
151        let c = now();
152        assert!(
153            a.to_canonical() < b.to_canonical(),
154            "{} !< {}",
155            a.to_canonical(),
156            b.to_canonical()
157        );
158        assert!(b.to_canonical() < c.to_canonical());
159    }
160
161    #[test]
162    fn observe_advances_past_far_future_remote() {
163        let remote = Hlc {
164            wall_ms: physical_now_ms() + 1_000_000, // ~16 min in the future
165            counter: 42,
166            node: "other".to_string(),
167        };
168        observe(&remote);
169        let next = now();
170        assert!(
171            next.to_canonical() > remote.to_canonical(),
172            "local clock must advance past an observed future remote: {} !> {}",
173            next.to_canonical(),
174            remote.to_canonical()
175        );
176    }
177
178    #[test]
179    fn canonical_sorts_chronologically_and_breaks_ties_by_node() {
180        let earlier = Hlc {
181            wall_ms: 100,
182            counter: 5,
183            node: "z".into(),
184        };
185        let later_wall = Hlc {
186            wall_ms: 101,
187            counter: 0,
188            node: "a".into(),
189        };
190        assert!(earlier.to_canonical() < later_wall.to_canonical());
191
192        let same_a = Hlc {
193            wall_ms: 100,
194            counter: 5,
195            node: "a".into(),
196        };
197        let same_b = Hlc {
198            wall_ms: 100,
199            counter: 5,
200            node: "b".into(),
201        };
202        assert!(
203            same_a.to_canonical() < same_b.to_canonical(),
204            "equal (wall,counter) must break by node"
205        );
206    }
207
208    #[test]
209    fn parse_roundtrips_including_dotted_node() {
210        let h = Hlc {
211            wall_ms: 1234567890,
212            counter: 7,
213            node: "laptop.local".into(),
214        };
215        let parsed = Hlc::parse(&h.to_canonical()).expect("parse");
216        assert_eq!(parsed, h);
217    }
218}