Skip to main content

lunaris_core/
hlc.rs

1//! Hybrid Logical Clock — wall-time millis + monotonic counter + node id.
2//!
3//! Issued timestamps are totally ordered across threads; under contention the
4//! counter increments instead of the wall clock so we never return duplicates.
5//!
6//! Storage layout (24 bytes):
7//!   wall_ms : u64  — unix millis at last advance
8//!   counter : u32  — incremented when wall did not advance
9//!   node_id : u16  — process / node identity (0 in single-node v0)
10//!   _pad    : u16
11//!
12//! Total order: (wall_ms, counter, node_id) lex comparison.
13
14use std::sync::Arc;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use parking_lot::Mutex;
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
21pub struct Hlc {
22    pub wall_ms: u64,
23    pub counter: u32,
24    pub node_id: u16,
25}
26
27impl Hlc {
28    pub const ZERO: Hlc = Hlc { wall_ms: 0, counter: 0, node_id: 0 };
29
30    pub fn from_parts(wall_ms: u64, counter: u32, node_id: u16) -> Self {
31        Self { wall_ms, counter, node_id }
32    }
33
34    /// An `Hlc` addressing a real-world instant, for use on the **valid**
35    /// axis of a [`crate::BiTemporal`].
36    ///
37    /// This is deliberately NOT issued by an [`HlcClock`]. A clock-issued
38    /// stamp answers "when did this process observe the event", which is the
39    /// **system** axis; the valid axis answers "when was this true in the
40    /// world" and routinely points into the past. Backdating the system axis
41    /// would break the total order the clock exists to guarantee — backdating
42    /// the valid axis is the whole point of having two.
43    ///
44    /// `counter` and `node_id` are zero: a real-world instant carries no
45    /// causality, so there is no tie to break. Two episodes stamped at the
46    /// same real-world millisecond compare equal on the valid axis, which is
47    /// the correct answer.
48    ///
49    /// Pre-epoch instants clamp to zero. `wall_ms` is unsigned, so the
50    /// alternative is a wrap into the far future — a document dated 1969
51    /// would sort after everything instead of before it.
52    pub fn from_utc(t: chrono::DateTime<chrono::Utc>) -> Self {
53        Self { wall_ms: t.timestamp_millis().max(0) as u64, counter: 0, node_id: 0 }
54    }
55}
56
57#[derive(Debug)]
58pub struct HlcClock {
59    inner: Mutex<Hlc>,
60    node_id: u16,
61}
62
63impl HlcClock {
64    pub fn new(node_id: u16) -> Arc<Self> {
65        Arc::new(Self {
66            inner: Mutex::new(Hlc { wall_ms: now_millis(), counter: 0, node_id }),
67            node_id,
68        })
69    }
70
71    /// Issue the next monotonic timestamp.
72    ///
73    /// Algorithm:
74    ///   wall = max(prev.wall_ms, system_now)
75    ///   if wall == prev.wall_ms: counter = prev.counter + 1
76    ///   else: counter = 0
77    ///
78    /// The lock is held only across pure CPU work — never across .await.
79    pub fn tick(&self) -> Hlc {
80        let mut g = self.inner.lock();
81        let now = now_millis();
82        if now > g.wall_ms {
83            g.wall_ms = now;
84            g.counter = 0;
85        } else {
86            // wall did not advance — bump counter
87            g.counter = g.counter.saturating_add(1);
88        }
89        let issued = *g;
90        Hlc { wall_ms: issued.wall_ms, counter: issued.counter, node_id: self.node_id }
91    }
92
93    pub fn node_id(&self) -> u16 {
94        self.node_id
95    }
96}
97
98#[inline]
99fn now_millis() -> u64 {
100    SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)
101}