1use std::sync::Mutex;
16use std::time::{SystemTime, UNIX_EPOCH};
17
18use serde::{Deserialize, Serialize};
19
20use crate::error::{ArrayError, ArrayResult};
21use crate::sync::replica_id::ReplicaId;
22
23pub const MAX_PHYSICAL_MS: u64 = (1u64 << 48) - 1;
29
30#[derive(
35 Copy,
36 Clone,
37 Debug,
38 PartialEq,
39 Eq,
40 Serialize,
41 Deserialize,
42 zerompk::ToMessagePack,
43 zerompk::FromMessagePack,
44)]
45pub struct Hlc {
46 pub physical_ms: u64,
49 pub logical: u16,
51 pub replica_id: ReplicaId,
53}
54
55impl Hlc {
56 pub const ZERO: Self = Self {
58 physical_ms: 0,
59 logical: 0,
60 replica_id: ReplicaId(0),
61 };
62
63 pub fn new(physical_ms: u64, logical: u16, replica_id: ReplicaId) -> ArrayResult<Self> {
66 if physical_ms > MAX_PHYSICAL_MS {
67 return Err(ArrayError::InvalidHlc {
68 detail: format!(
69 "physical_ms {physical_ms} exceeds MAX_PHYSICAL_MS {MAX_PHYSICAL_MS}"
70 ),
71 });
72 }
73 Ok(Self {
74 physical_ms,
75 logical,
76 replica_id,
77 })
78 }
79
80 pub fn is_later_than(&self, other: &Self) -> bool {
90 (self.physical_ms, self.logical) > (other.physical_ms, other.logical)
91 }
92
93 pub fn to_bytes(&self) -> [u8; 18] {
97 let mut out = [0u8; 18];
98 out[0..8].copy_from_slice(&self.physical_ms.to_be_bytes());
99 out[8..10].copy_from_slice(&self.logical.to_be_bytes());
100 out[10..18].copy_from_slice(&self.replica_id.0.to_be_bytes());
101 out
102 }
103
104 pub fn from_bytes(b: &[u8; 18]) -> Self {
109 let physical_ms = u64::from_be_bytes(
110 b[0..8]
111 .try_into()
112 .expect("invariant: b is [u8; 18], slice [0..8] is exactly 8 bytes"),
113 );
114 let logical = u16::from_be_bytes(
115 b[8..10]
116 .try_into()
117 .expect("invariant: b is [u8; 18], slice [8..10] is exactly 2 bytes"),
118 );
119 let replica_id =
120 ReplicaId(u64::from_be_bytes(b[10..18].try_into().expect(
121 "invariant: b is [u8; 18], slice [10..18] is exactly 8 bytes",
122 )));
123 Self {
124 physical_ms,
125 logical,
126 replica_id,
127 }
128 }
129}
130
131impl PartialOrd for Hlc {
132 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
133 Some(self.cmp(other))
134 }
135}
136
137impl Ord for Hlc {
138 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
139 (self.physical_ms, self.logical, self.replica_id.0).cmp(&(
140 other.physical_ms,
141 other.logical,
142 other.replica_id.0,
143 ))
144 }
145}
146
147pub struct HlcGenerator {
152 replica_id: ReplicaId,
153 state: Mutex<(u64, u16)>,
155}
156
157impl HlcGenerator {
158 pub fn new(replica_id: ReplicaId) -> Self {
160 Self {
161 replica_id,
162 state: Mutex::new((0, 0)),
163 }
164 }
165
166 fn now_ms() -> ArrayResult<u64> {
168 SystemTime::now()
169 .duration_since(UNIX_EPOCH)
170 .map(|d| d.as_millis() as u64)
171 .map_err(|e| ArrayError::InvalidHlc {
172 detail: format!("system clock before Unix epoch: {e}"),
173 })
174 }
175
176 pub fn next(&self) -> ArrayResult<Hlc> {
183 let now_ms = Self::now_ms()?;
184 if now_ms > MAX_PHYSICAL_MS {
185 return Err(ArrayError::InvalidHlc {
186 detail: format!("system clock {now_ms} exceeds MAX_PHYSICAL_MS"),
187 });
188 }
189
190 let mut guard = self.state.lock().map_err(|_| ArrayError::HlcLockPoisoned)?;
191 let (last_physical, last_logical) = *guard;
192
193 let new_physical = now_ms.max(last_physical);
194 let new_logical = if new_physical == last_physical {
195 last_logical
196 .checked_add(1)
197 .ok_or_else(|| ArrayError::InvalidHlc {
198 detail: "logical counter overflow within one millisecond".into(),
199 })?
200 } else {
201 0
202 };
203
204 *guard = (new_physical, new_logical);
205 drop(guard);
206
207 Hlc::new(new_physical, new_logical, self.replica_id)
208 }
209
210 pub fn observe(&self, remote: Hlc) -> ArrayResult<()> {
216 let now_ms = Self::now_ms()?.min(MAX_PHYSICAL_MS);
217
218 let mut guard = self.state.lock().map_err(|_| ArrayError::HlcLockPoisoned)?;
219 let (last_physical, last_logical) = *guard;
220
221 let new_physical = now_ms.max(last_physical).max(remote.physical_ms);
222 let new_logical = if new_physical == last_physical && new_physical == remote.physical_ms {
223 last_logical
225 .max(remote.logical)
226 .checked_add(1)
227 .ok_or_else(|| ArrayError::InvalidHlc {
228 detail: "logical counter overflow during observe".into(),
229 })?
230 } else if new_physical == last_physical {
231 last_logical
232 .checked_add(1)
233 .ok_or_else(|| ArrayError::InvalidHlc {
234 detail: "logical counter overflow during observe (local wins)".into(),
235 })?
236 } else if new_physical == remote.physical_ms {
237 remote
238 .logical
239 .checked_add(1)
240 .ok_or_else(|| ArrayError::InvalidHlc {
241 detail: "logical counter overflow during observe (remote wins)".into(),
242 })?
243 } else {
244 0
246 };
247
248 *guard = (new_physical, new_logical);
249 Ok(())
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn test_replica() -> ReplicaId {
258 ReplicaId::new(42)
259 }
260
261 #[test]
262 fn monotonic_under_fast_calls() {
263 let g = HlcGenerator::new(test_replica());
264 let mut prev = g.next().unwrap();
265 for _ in 0..999 {
266 let curr = g.next().unwrap();
267 assert!(
268 curr > prev,
269 "HLC must be strictly increasing: {curr:?} <= {prev:?}"
270 );
271 prev = curr;
272 }
273 }
274
275 #[test]
276 fn survives_clock_skew_injection() {
277 let g = HlcGenerator::new(test_replica());
278
279 let baseline = g.next().unwrap();
281
282 let future_physical = baseline.physical_ms + 100_000; let future_hlc = Hlc::new(future_physical, 50, ReplicaId::new(99)).unwrap();
285
286 g.observe(future_hlc).unwrap();
288
289 let next = g.next().unwrap();
291 assert!(
292 next > future_hlc,
293 "next {next:?} should be > observed future {future_hlc:?}"
294 );
295 }
296
297 #[test]
298 fn to_bytes_roundtrip() {
299 let hlc = Hlc::new(123_456_789, 7, ReplicaId::new(0xabcd)).unwrap();
300 let bytes = hlc.to_bytes();
301 let back = Hlc::from_bytes(&bytes);
302 assert_eq!(hlc, back);
303 }
304
305 #[test]
306 fn byte_order_matches_lexicographic() {
307 use std::collections::BTreeMap;
308
309 let replica = ReplicaId::new(1);
311 let mut hlcs: Vec<Hlc> = (0u64..100)
312 .map(|i| Hlc {
313 physical_ms: i / 10,
314 logical: (i % 10) as u16,
315 replica_id: replica,
316 })
317 .collect();
318
319 let mut by_ord = hlcs.clone();
321 by_ord.sort();
322
323 hlcs.sort_by_key(|h| h.to_bytes());
325
326 assert_eq!(by_ord, hlcs, "byte sort must match Ord sort");
327
328 let mut map: BTreeMap<[u8; 18], Hlc> = BTreeMap::new();
330 for h in &by_ord {
331 map.insert(h.to_bytes(), *h);
332 }
333 let map_order: Vec<Hlc> = map.into_values().collect();
334 assert_eq!(by_ord, map_order);
335 }
336
337 #[test]
338 fn serialize_roundtrip() {
339 let hlc = Hlc::new(9_999, 3, ReplicaId::new(77)).unwrap();
340 let bytes = zerompk::to_msgpack_vec(&hlc).expect("serialize");
341 let back: Hlc = zerompk::from_msgpack(&bytes).expect("deserialize");
342 assert_eq!(hlc, back);
343 }
344
345 #[test]
346 fn physical_ms_overflow_errors() {
347 let result = Hlc::new(MAX_PHYSICAL_MS + 1, 0, ReplicaId::new(1));
348 assert!(
349 matches!(result, Err(ArrayError::InvalidHlc { .. })),
350 "expected InvalidHlc, got: {result:?}"
351 );
352 }
353
354 #[test]
355 fn hlc_zero_is_minimum() {
356 let non_zero = Hlc::new(1, 0, ReplicaId::new(0)).unwrap();
357 assert!(Hlc::ZERO < non_zero);
358 }
359}