1use std::cmp::Ordering;
14use std::sync::Mutex;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use serde::{Deserialize, Serialize};
18
19pub const MAX_CLOCK_SKEW_NS: u64 = 5_000_000_000;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct ClockSkew {
30 pub remote_wall_ns: u64,
32 pub local_wall_ns: u64,
34 pub skew_ns: u64,
36}
37
38impl std::fmt::Display for ClockSkew {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(
41 f,
42 "remote HLC wall clock is {}ms ahead of local (remote {}ns, local {}ns); \
43 the maximum accepted skew is {}ms",
44 self.skew_ns / 1_000_000,
45 self.remote_wall_ns,
46 self.local_wall_ns,
47 MAX_CLOCK_SKEW_NS / 1_000_000
48 )
49 }
50}
51
52impl std::error::Error for ClockSkew {}
53
54#[non_exhaustive]
65#[derive(
66 Debug,
67 Clone,
68 Copy,
69 Default,
70 PartialEq,
71 Eq,
72 Hash,
73 Serialize,
74 Deserialize,
75 zerompk::ToMessagePack,
76 zerompk::FromMessagePack,
77)]
78pub struct Hlc {
79 pub wall_ns: u64,
81 pub logical: u64,
83}
84
85impl Hlc {
86 pub const ZERO: Hlc = Hlc {
87 wall_ns: 0,
88 logical: 0,
89 };
90
91 pub const fn new(wall_ns: u64, logical: u64) -> Self {
92 Self { wall_ns, logical }
93 }
94}
95
96impl PartialOrd for Hlc {
97 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
98 Some(self.cmp(other))
99 }
100}
101
102impl Ord for Hlc {
103 fn cmp(&self, other: &Self) -> Ordering {
104 match self.wall_ns.cmp(&other.wall_ns) {
105 Ordering::Equal => self.logical.cmp(&other.logical),
106 other => other,
107 }
108 }
109}
110
111#[derive(Debug, Default)]
113pub struct HlcClock {
114 state: Mutex<Hlc>,
115}
116
117impl HlcClock {
118 pub fn new() -> Self {
119 Self {
120 state: Mutex::new(Hlc::ZERO),
121 }
122 }
123
124 pub fn now(&self) -> Hlc {
127 let wall = wall_now_ns();
128 let mut st = self.state.lock().unwrap_or_else(|p| p.into_inner());
129 let wall = wall.max(st.wall_ns);
132 let next = if wall > st.wall_ns {
133 Hlc::new(wall, 0)
134 } else {
135 Hlc::new(st.wall_ns, st.logical + 1)
136 };
137 *st = next;
138 next
139 }
140
141 pub fn update(&self, remote: Hlc) -> Hlc {
144 let wall = wall_now_ns();
145 let mut st = self.state.lock().unwrap_or_else(|p| p.into_inner());
146 let prev = *st;
147 let max_wall = wall.max(prev.wall_ns).max(remote.wall_ns);
148 let next_logical = if max_wall == prev.wall_ns && max_wall == remote.wall_ns {
149 prev.logical.max(remote.logical) + 1
150 } else if max_wall == prev.wall_ns {
151 prev.logical + 1
152 } else if max_wall == remote.wall_ns {
153 remote.logical + 1
154 } else {
155 0
156 };
157 let next = Hlc::new(max_wall, next_logical);
158 *st = next;
159 next
160 }
161
162 pub fn update_checked(&self, remote: Hlc) -> Result<Hlc, ClockSkew> {
173 let wall = wall_now_ns();
174 let skew = remote.wall_ns.saturating_sub(wall);
175 if skew > MAX_CLOCK_SKEW_NS {
176 return Err(ClockSkew {
177 remote_wall_ns: remote.wall_ns,
178 local_wall_ns: wall,
179 skew_ns: skew,
180 });
181 }
182 Ok(self.update(remote))
183 }
184
185 pub fn peek(&self) -> Hlc {
187 *self.state.lock().unwrap_or_else(|p| p.into_inner())
188 }
189}
190
191fn wall_now_ns() -> u64 {
192 SystemTime::now()
193 .duration_since(UNIX_EPOCH)
194 .map(|d| d.as_nanos() as u64)
195 .unwrap_or_else(|_| {
196 use std::sync::atomic::{AtomicBool, Ordering};
197 static LOGGED: AtomicBool = AtomicBool::new(false);
198 if !LOGGED.swap(true, Ordering::Relaxed) {
199 tracing::error!(
200 module = module_path!(),
201 "system clock is before UNIX_EPOCH; using 0 (epoch) \
202 — check NTP/RTC configuration"
203 );
204 }
205 0
206 })
207}
208
209#[cfg(test)]
210mod tests {
211
212 #[test]
214 fn a_remote_hlc_inside_the_bound_is_accepted() {
215 let clock = HlcClock::new();
216 let local = clock.now();
217 let remote = Hlc::new(local.wall_ns + 1_000_000, 0);
218 let folded = clock
219 .update_checked(remote)
220 .expect("a remote clock 1ms ahead is ordinary skew");
221 assert!(folded > remote, "the fold must exceed the observation");
222 assert!(clock.peek() >= folded);
223 }
224
225 #[test]
227 fn a_remote_hlc_past_the_bound_is_refused_without_moving_the_clock() {
228 let clock = HlcClock::new();
229 let before = clock.peek();
230 let far_future = Hlc::new(wall_now_ns() + MAX_CLOCK_SKEW_NS * 4, 0);
231 let err = clock
232 .update_checked(far_future)
233 .expect_err("a clock four times the bound ahead must be refused");
234 assert!(err.skew_ns > MAX_CLOCK_SKEW_NS);
235 assert!(
236 clock.peek() < far_future,
237 "a refused observation must never advance the clock"
238 );
239 assert!(clock.peek() >= before);
240 }
241
242 #[test]
245 fn a_remote_hlc_in_the_past_is_never_refused() {
246 let clock = HlcClock::new();
247 let past = Hlc::new(wall_now_ns().saturating_sub(MAX_CLOCK_SKEW_NS * 10), 0);
248 assert!(clock.update_checked(past).is_ok());
249 }
250 use super::*;
251
252 #[test]
253 fn monotonic_within_tick() {
254 let clock = HlcClock::new();
255 let mut prev = clock.now();
256 for _ in 0..1_000 {
257 let next = clock.now();
258 assert!(next > prev);
259 prev = next;
260 }
261 }
262
263 #[test]
264 fn update_produces_strictly_greater() {
265 let clock = HlcClock::new();
266 let local = clock.now();
267 let remote = Hlc::new(local.wall_ns + 1_000_000, 7);
268 let merged = clock.update(remote);
269 assert!(merged > remote);
270 assert!(merged > local);
271 }
272
273 #[test]
274 fn ordering_is_total() {
275 let a = Hlc::new(10, 0);
276 let b = Hlc::new(10, 1);
277 let c = Hlc::new(11, 0);
278 assert!(a < b);
279 assert!(b < c);
280 assert!(a < c);
281 }
282
283 #[test]
287 fn burst_strictly_monotonic() {
288 let clock = HlcClock::new();
289 let mut prev = clock.now();
290 for _ in 0..100_000 {
291 let next = clock.now();
292 assert!(
293 next > prev,
294 "monotonicity violated: prev={prev:?} next={next:?}"
295 );
296 prev = next;
297 }
298 }
299
300 #[test]
305 fn saturating_add_regression() {
306 let clock = HlcClock::new();
307 let seed = clock.now();
309 {
312 let mut st = clock.state.lock().unwrap_or_else(|p| p.into_inner());
313 *st = Hlc::new(seed.wall_ns, u32::MAX as u64);
314 }
315 let next = clock.now();
316 assert!(
317 next > Hlc::new(seed.wall_ns, u32::MAX as u64),
318 "logical counter pinned at u32::MAX: next={next:?}"
319 );
320 }
321
322 #[test]
325 fn update_burst_strictly_monotonic() {
326 let clock = HlcClock::new();
327 let remote = clock.now();
328 let mut prev = clock.update(remote);
329 for _ in 0..100_000 {
330 let next = clock.update(remote);
331 assert!(
332 next > prev,
333 "update monotonicity violated: prev={prev:?} next={next:?}"
334 );
335 prev = next;
336 }
337 }
338
339 #[test]
342 fn roundtrip_logical_gt_u32_max() {
343 let logical_val = (u32::MAX as u64) + 1;
344 let original = Hlc::new(1_700_000_000_000_000_000_u64, logical_val);
345 let bytes = zerompk::to_msgpack_vec(&original).expect("encode");
346 let decoded: Hlc = zerompk::from_msgpack(&bytes).expect("decode");
347 assert_eq!(decoded, original);
348 assert_eq!(decoded.logical, logical_val);
349 }
350}