1use std::sync::{Arc, Mutex};
25use std::time::{SystemTime, UNIX_EPOCH};
26
27pub const HIGHWATER_STATE_KEY: &str = "hlc_highwater";
31
32pub const MAX_FUTURE_SKEW_MS: u64 = 30 * 24 * 60 * 60 * 1000;
41
42pub const COUNTER_MAX: u16 = 9999;
45
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
48pub struct Timestamp {
49 pub millis: u64,
50 pub counter: u16,
51 pub device_id: String,
52}
53
54impl Timestamp {
55 pub fn new(millis: u64, counter: u16, device_id: String) -> Self {
56 Self {
57 millis,
58 counter,
59 device_id,
60 }
61 }
62
63 pub fn is_within_future_bound(&self, receiver_wall_ms: u64) -> bool {
70 self.millis <= receiver_wall_ms.saturating_add(MAX_FUTURE_SKEW_MS)
71 }
72
73 pub fn parse(s: &str) -> Option<Self> {
75 let mut parts = s.splitn(3, '-');
76 let millis = parts.next()?.parse::<u64>().ok()?;
77 let counter = parts.next()?.parse::<u16>().ok()?;
78 let device_id = parts.next()?;
79 if device_id.is_empty() {
80 return None;
81 }
82 if counter > COUNTER_MAX {
83 return None;
84 }
85 Some(Self {
86 millis,
87 counter,
88 device_id: device_id.to_string(),
89 })
90 }
91}
92
93impl std::fmt::Display for Timestamp {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 write!(
96 f,
97 "{:013}-{:04}-{}",
98 self.millis, self.counter, self.device_id
99 )
100 }
101}
102
103struct HlcState {
104 millis: u64,
105 counter: u16,
106}
107
108fn increment(state: &mut HlcState) {
109 if state.counter < COUNTER_MAX {
110 state.counter += 1;
111 } else if let Some(next_millis) = state.millis.checked_add(1) {
112 state.millis = next_millis;
113 state.counter = 0;
114 } else {
115 state.counter = COUNTER_MAX;
116 }
117}
118
119pub struct Hlc {
124 device_id: String,
125 state: Mutex<HlcState>,
126 wall_clock: Box<dyn Fn() -> u64 + Send + Sync>,
128}
129
130impl Hlc {
131 pub fn try_new(device_id: String) -> Result<Self, crate::store_dir::PathTokenError> {
132 Self::try_new_with_wall_clock(device_id, wall_clock_ms)
133 }
134
135 pub fn new(device_id: String) -> Self {
137 Self::try_new(device_id).expect("device_id must be a safe path token")
138 }
139
140 pub(crate) fn device_id(&self) -> &str {
141 &self.device_id
142 }
143
144 fn try_new_with_wall_clock(
145 device_id: String,
146 clock: impl Fn() -> u64 + Send + Sync + 'static,
147 ) -> Result<Self, crate::store_dir::PathTokenError> {
148 crate::store_dir::validate_path_token(&device_id)?;
149 Ok(Self::new_validated(device_id, Box::new(clock)))
150 }
151
152 fn new_validated(device_id: String, wall_clock: Box<dyn Fn() -> u64 + Send + Sync>) -> Self {
153 Self {
154 device_id,
155 state: Mutex::new(HlcState {
156 millis: 0,
157 counter: 0,
158 }),
159 wall_clock,
160 }
161 }
162
163 pub fn seed(&self, high_water: &Timestamp) {
170 let mut state = self.state.lock().unwrap();
171 if high_water.millis > state.millis
172 || (high_water.millis == state.millis && high_water.counter > state.counter)
173 {
174 state.millis = high_water.millis;
175 state.counter = high_water.counter;
176 }
177 }
178
179 pub fn high_water(&self) -> Timestamp {
184 let state = self.state.lock().unwrap();
185 Timestamp::new(state.millis, state.counter, self.device_id.clone())
186 }
187
188 pub fn wall_now_ms(&self) -> u64 {
195 (self.wall_clock)()
196 }
197
198 pub fn now(&self) -> Timestamp {
201 let wall = (self.wall_clock)();
202 let mut state = self.state.lock().unwrap();
203
204 if wall > state.millis {
205 state.millis = wall;
206 state.counter = 0;
207 } else {
208 increment(&mut state);
209 }
210
211 Timestamp::new(state.millis, state.counter, self.device_id.clone())
212 }
213
214 pub fn advance_past(&self, remote: &Timestamp) {
224 let wall = (self.wall_clock)();
225 let mut state = self.state.lock().unwrap();
226
227 if wall > state.millis && wall > remote.millis {
228 state.millis = wall;
230 state.counter = 0;
231 } else if remote.millis > state.millis {
232 state.millis = remote.millis;
234 state.counter = remote.counter;
235 } else if state.millis == remote.millis && remote.counter > state.counter {
236 state.counter = remote.counter;
238 }
239 }
240
241 #[cfg(test)]
242 pub(crate) fn with_wall_clock(
243 device_id: String,
244 clock: impl Fn() -> u64 + Send + Sync + 'static,
245 ) -> Self {
246 Self::try_new_with_wall_clock(device_id, clock)
247 .expect("device_id must be a safe path token")
248 }
249}
250
251#[derive(Clone)]
268pub struct UpdatedAtStamper {
269 hlc: Arc<Hlc>,
270}
271
272impl UpdatedAtStamper {
273 pub fn new(hlc: Arc<Hlc>) -> Self {
274 Self { hlc }
275 }
276
277 pub fn stamp(&self) -> String {
281 self.hlc.now().to_string()
282 }
283
284 #[cfg(feature = "test-utils")]
289 pub fn for_test() -> Self {
290 Self::new(Arc::new(Hlc::new("test-device".to_string())))
291 }
292}
293
294#[cfg(test)]
301pub fn now_wall_ms() -> u64 {
302 wall_clock_ms()
303}
304
305fn wall_clock_ms() -> u64 {
307 SystemTime::now()
308 .duration_since(UNIX_EPOCH)
309 .expect("system clock before UNIX epoch")
310 .as_millis() as u64
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use std::sync::atomic::{AtomicU64, Ordering};
317
318 fn fixed_clock(ms: u64) -> impl Fn() -> u64 + Send + Sync + 'static {
319 move || ms
320 }
321
322 fn advancing_clock(start: u64) -> (Arc<AtomicU64>, impl Fn() -> u64 + Send + Sync + 'static) {
323 let time = Arc::new(AtomicU64::new(start));
324 let time_clone = time.clone();
325 (time, move || time_clone.load(Ordering::SeqCst))
326 }
327
328 #[test]
329 fn basic_monotonicity() {
330 let hlc = Hlc::new("dev-1".into());
331 let t1 = hlc.now();
332 let t2 = hlc.now();
333 let t3 = hlc.now();
334
335 assert!(t2 > t1, "t2={t2} should be > t1={t1}");
336 assert!(t3 > t2, "t3={t3} should be > t2={t2}");
337 }
338
339 #[test]
340 fn new_rejects_empty_device_id() {
341 assert!(matches!(
342 Hlc::try_new(String::new()),
343 Err(crate::store_dir::PathTokenError::Empty),
344 ));
345 }
346
347 #[test]
348 fn counter_increments_when_clock_stalls() {
349 let hlc = Hlc::with_wall_clock("dev-1".into(), fixed_clock(1000));
350
351 let t1 = hlc.now();
352 assert_eq!(t1.millis, 1000);
353 assert_eq!(t1.counter, 0);
354
355 let t2 = hlc.now();
356 assert_eq!(t2.millis, 1000);
357 assert_eq!(t2.counter, 1);
358
359 let t3 = hlc.now();
360 assert_eq!(t3.millis, 1000);
361 assert_eq!(t3.counter, 2);
362
363 assert!(t3 > t2);
364 assert!(t2 > t1);
365 }
366
367 #[test]
368 fn wall_clock_advance_resets_counter() {
369 let (time, clock) = advancing_clock(1000);
370 let hlc = Hlc::with_wall_clock("dev-1".into(), clock);
371
372 let t1 = hlc.now();
373 assert_eq!(t1.millis, 1000);
374 assert_eq!(t1.counter, 0);
375
376 let t2 = hlc.now();
378 assert_eq!(t2.counter, 1);
379
380 time.store(2000, Ordering::SeqCst);
382 let t3 = hlc.now();
383 assert_eq!(t3.millis, 2000);
384 assert_eq!(t3.counter, 0);
385
386 assert!(t3 > t2);
387 }
388
389 #[test]
390 fn advance_past_remote_ahead() {
391 let hlc = Hlc::with_wall_clock("dev-local".into(), fixed_clock(1000));
392
393 let remote = Timestamp::new(5000, 3, "dev-remote".into());
395 hlc.advance_past(&remote);
396
397 let t = hlc.now();
399 assert!(
400 t.to_string() > remote.to_string(),
401 "t={t} must beat {remote}"
402 );
403 assert_eq!(t.millis, 5000);
404 assert_eq!(t.device_id, "dev-local");
405 }
406
407 #[test]
408 fn advance_past_remote_behind() {
409 let hlc = Hlc::with_wall_clock("dev-local".into(), fixed_clock(5000));
410
411 let primed = hlc.now();
413
414 let remote = Timestamp::new(1000, 10, "dev-remote".into());
416 hlc.advance_past(&remote);
417
418 let t = hlc.now();
419 assert!(
420 t > primed,
421 "t={t} must stay above the primed clock {primed}"
422 );
423 assert_eq!(t.millis, 5000);
424 }
425
426 #[test]
433 fn advance_past_far_future_applied_row_is_not_capped() {
434 let hlc = Hlc::with_wall_clock("dev-local".into(), fixed_clock(1000));
435
436 let far_future = 1000 + 48 * 60 * 60 * 1000;
439 let applied = Timestamp::new(far_future, 7, "dev-remote".into());
440 hlc.advance_past(&applied);
441
442 let next = hlc.now();
445 assert!(
446 next.to_string() > applied.to_string(),
447 "next stamp {next} regressed below applied row {applied}: the clock \
448 refused to advance past an authoritative register value",
449 );
450 assert_eq!(next.millis, far_future);
451 }
452
453 #[test]
454 fn string_roundtrip() {
455 let ts = Timestamp::new(1707580800000, 42, "dev-abc123".into());
456 let s = ts.to_string();
457 let parsed = Timestamp::parse(&s).expect("parse should succeed");
458
459 assert_eq!(parsed, ts);
460 assert_eq!(s, "1707580800000-0042-dev-abc123");
461 }
462
463 #[test]
464 fn string_format_is_zero_padded() {
465 let ts = Timestamp::new(1000, 0, "d".into());
466 assert_eq!(ts.to_string(), "0000000001000-0000-d");
467
468 let ts2 = Timestamp::new(9999999999999, 9999, "d".into());
469 assert_eq!(ts2.to_string(), "9999999999999-9999-d");
470 }
471
472 #[test]
473 fn lexicographic_ordering_matches_causal_ordering() {
474 let timestamps = [
475 Timestamp::new(1000, 0, "dev-a".into()),
476 Timestamp::new(1000, 1, "dev-a".into()),
477 Timestamp::new(1000, 1, "dev-b".into()),
478 Timestamp::new(2000, 0, "dev-a".into()),
479 Timestamp::new(2000, 0, "dev-b".into()),
480 ];
481
482 let strings: Vec<String> = timestamps.iter().map(|t| t.to_string()).collect();
483
484 for i in 1..strings.len() {
486 assert!(
487 strings[i] > strings[i - 1],
488 "Expected {:?} > {:?}",
489 strings[i],
490 strings[i - 1]
491 );
492 }
493 }
494
495 #[test]
496 fn device_id_breaks_ties() {
497 let ts_a = Timestamp::new(5000, 3, "aaa".into());
498 let ts_b = Timestamp::new(5000, 3, "bbb".into());
499
500 assert!(ts_b > ts_a);
502
503 assert!(ts_b.to_string() > ts_a.to_string());
505 }
506
507 #[test]
508 fn future_bound_admits_honest_and_rejects_grossly_future() {
509 let wall: u64 = 1_700_000_000_000;
510
511 assert!(Timestamp::new(wall, 0, "d".into()).is_within_future_bound(wall));
513 assert!(Timestamp::new(0, 0, "d".into()).is_within_future_bound(wall));
514
515 let just_inside = wall + MAX_FUTURE_SKEW_MS - 1;
517 assert!(Timestamp::new(just_inside, 0, "d".into()).is_within_future_bound(wall));
518 let at_bound = wall + MAX_FUTURE_SKEW_MS;
520 assert!(Timestamp::new(at_bound, 0, "d".into()).is_within_future_bound(wall));
521
522 let just_beyond = wall + MAX_FUTURE_SKEW_MS + 1;
524 assert!(!Timestamp::new(just_beyond, 0, "d".into()).is_within_future_bound(wall));
525 assert!(!Timestamp::new(u64::MAX, 0, "d".into()).is_within_future_bound(wall));
527
528 assert!(Timestamp::new(u64::MAX, 0, "d".into()).is_within_future_bound(u64::MAX));
531 }
532
533 #[test]
534 fn parse_rejects_invalid_input() {
535 assert!(Timestamp::parse("").is_none());
536 assert!(Timestamp::parse("not-a-timestamp").is_none());
537 assert!(Timestamp::parse("1000-0000").is_none()); assert!(Timestamp::parse("1000-0000-").is_none()); assert!(Timestamp::parse("abc-0000-dev").is_none()); assert!(Timestamp::parse("1000-xyz-dev").is_none()); }
542
543 #[test]
544 fn parse_rejects_counter_above_format_width() {
545 assert!(Timestamp::parse("0000000001000-10000-dev").is_none());
546 assert!(Timestamp::parse("0000000001000-65535-dev").is_none());
547 }
548
549 #[test]
550 fn advance_past_counter_bound_carries_into_millis() {
551 let hlc = Hlc::with_wall_clock("dev-local".into(), fixed_clock(1000));
552 let observed = Timestamp::new(1000, 9999, "dev-remote".into());
553
554 hlc.advance_past(&observed);
555 let next = hlc.now();
556
557 assert!(
558 next.to_string() > observed.to_string(),
559 "next stamp {next} must sort after observed stamp {observed}",
560 );
561 assert_eq!(next.millis, 1001);
562 assert_eq!(next.counter, 0);
563 }
564
565 #[test]
566 fn now_counter_bound_carries_into_millis() {
567 let hlc = Hlc::with_wall_clock("dev-local".into(), fixed_clock(1000));
568 hlc.seed(&Timestamp::new(1000, 9999, "dev-remote".into()));
569
570 let next = hlc.now();
571
572 assert_eq!(next.millis, 1001);
573 assert_eq!(next.counter, 0);
574 assert_eq!(next.to_string(), "0000000001001-0000-dev-local");
575 }
576
577 #[test]
578 fn minted_counters_keep_fixed_width_lexical_order() {
579 let hlc = Hlc::with_wall_clock("dev-local".into(), fixed_clock(1000));
580 hlc.seed(&Timestamp::new(1000, 9998, "dev-remote".into()));
581
582 let stamps = [hlc.now(), hlc.now(), hlc.now()];
583 for stamp in &stamps {
584 let rendered = stamp.to_string();
585 let counter = rendered
586 .split('-')
587 .nth(1)
588 .expect("timestamp has a counter field");
589 assert_eq!(counter.len(), 4);
590 }
591 for pair in stamps.windows(2) {
592 assert!(
593 pair[1] > pair[0],
594 "timestamp ordering must advance from {} to {}",
595 pair[0],
596 pair[1],
597 );
598 assert!(
599 pair[1].to_string() > pair[0].to_string(),
600 "string ordering must advance from {} to {}",
601 pair[0],
602 pair[1],
603 );
604 }
605 }
606
607 #[test]
608 fn parse_handles_device_id_with_dashes() {
609 let ts = Timestamp::new(1000, 0, "550e8400-e29b-41d4-a716-446655440000".into());
612 let s = ts.to_string();
613 let parsed = Timestamp::parse(&s).expect("parse should handle UUID device_id");
614 assert_eq!(parsed.device_id, "550e8400-e29b-41d4-a716-446655440000");
615 assert_eq!(parsed, ts);
616 }
617}