1use std::sync::{Mutex, OnceLock};
21use std::time::{SystemTime, UNIX_EPOCH};
22
23#[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 pub fn to_canonical(&self) -> String {
34 format!("{:013}.{:010}.{}", self.wall_ms, self.counter, self.node)
35 }
36
37 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
68fn node_cell() -> &'static OnceLock<String> {
71 static NODE: OnceLock<String> = OnceLock::new();
72 &NODE
73}
74
75pub 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
99pub 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 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
118pub 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 st.counter = st.counter.max(remote.counter).saturating_add(1);
129 } else if max_wall == remote.wall_ms {
130 st.wall_ms = max_wall;
132 st.counter = remote.counter.saturating_add(1);
133 } else if max_wall == st.wall_ms {
134 st.counter = st.counter.saturating_add(1);
136 } else {
137 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, 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}