1use std::fs::{File, OpenOptions};
53use std::path::Path;
54use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
55
56use memmap2::{MmapMut, MmapOptions};
57
58pub const FENCE_CLOCK_MAGIC: u64 = 0x4150_5546_434C_4B30;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct Hlc {
63 pub physical_us: u64,
64 pub logical: u64,
65}
66
67impl PartialOrd for Hlc {
68 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
69 Some(self.cmp(other))
70 }
71}
72impl Ord for Hlc {
73 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
74 match self.physical_us.cmp(&other.physical_us) {
75 std::cmp::Ordering::Equal => self.logical.cmp(&other.logical),
76 other => other,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum FenceClockError {
83 Full,
84 LayoutMismatch,
85 InvalidSlot,
86 IoError(std::io::ErrorKind),
87}
88
89impl From<std::io::Error> for FenceClockError {
90 fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
91}
92
93pub const EMPTY_PID: u32 = 0;
94
95#[repr(C, align(64))]
96pub struct HlcHeader {
97 pub magic: u64,
98 pub capacity: u64,
99 pub global_fence_physical: AtomicU64,
100 pub global_fence_logical: AtomicU64,
101 pub last_fence_epoch: AtomicU64,
102 pub cached_us: AtomicU64,
110 _pad: [u8; 8],
111}
112
113#[repr(C, align(64))]
114pub struct HlcSlot {
115 pub pid: AtomicU32,
116 _pad1: [u8; 4],
117 pub physical_us: AtomicU64,
118 pub logical: AtomicU64,
119 pub last_updated_us: AtomicU64,
120 _pad2: [u8; 32],
121}
122
123const _: () = {
124 assert!(std::mem::size_of::<HlcHeader>() == 64);
125 assert!(std::mem::size_of::<HlcSlot>() == 64);
126};
127
128pub const fn fence_clock_file_size(capacity: usize) -> usize {
129 std::mem::size_of::<HlcHeader>() + capacity * std::mem::size_of::<HlcSlot>()
130}
131
132pub struct SharedFenceClock {
133 _file: File,
134 mmap: MmapMut,
135 capacity: usize,
136 header_sidecar: subetha_core::HandshakeHeader,
137 ring_sidecar: Box<subetha_core::ObservationRing>,
138}
139
140unsafe impl Send for SharedFenceClock {}
141unsafe impl Sync for SharedFenceClock {}
142
143impl subetha_sidecar::AdaptiveInstance for SharedFenceClock {
144 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
145 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
146 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
147 Box::new(subetha_sidecar::NoMigrationPolicy)
148 }
149}
150
151
152impl SharedFenceClock {
153 pub fn create(path: impl AsRef<Path>, capacity: usize) -> Result<Self, FenceClockError> {
154 assert!(capacity >= 1);
155 crate::cached_clock::start();
158 let total = fence_clock_file_size(capacity);
159 let file = OpenOptions::new()
160 .read(true).write(true).create(true).truncate(true)
161 .open(path.as_ref())?;
162 file.set_len(total as u64)?;
163 let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
164 let hdr = mmap.as_mut_ptr() as *mut HlcHeader;
165 unsafe {
166 std::ptr::write(hdr, HlcHeader {
167 magic: FENCE_CLOCK_MAGIC,
168 capacity: capacity as u64,
169 global_fence_physical: AtomicU64::new(0),
170 global_fence_logical: AtomicU64::new(0),
171 last_fence_epoch: AtomicU64::new(0),
172 cached_us: AtomicU64::new(0),
173 _pad: [0; 8],
174 });
175 }
176 for i in 0..capacity {
177 let slot_ptr = unsafe {
178 mmap.as_mut_ptr()
179 .add(std::mem::size_of::<HlcHeader>())
180 .add(i * std::mem::size_of::<HlcSlot>())
181 } as *mut HlcSlot;
182 unsafe {
183 std::ptr::write(slot_ptr, HlcSlot {
184 pid: AtomicU32::new(EMPTY_PID),
185 _pad1: [0; 4],
186 physical_us: AtomicU64::new(0),
187 logical: AtomicU64::new(0),
188 last_updated_us: AtomicU64::new(0),
189 _pad2: [0; 32],
190 });
191 }
192 }
193 Ok(Self {
194 _file: file, mmap, capacity,
195 header_sidecar: subetha_core::HandshakeHeader::new(),
196 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
197 })
198 }
199
200 pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> Result<Self, FenceClockError> {
201 crate::cached_clock::start();
202 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
203 let total = fence_clock_file_size(expected_capacity);
204 if file.metadata()?.len() < total as u64 {
205 return Err(FenceClockError::LayoutMismatch);
206 }
207 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
208 let hdr = unsafe { &*(mmap.as_ptr() as *const HlcHeader) };
209 if hdr.magic != FENCE_CLOCK_MAGIC || hdr.capacity != expected_capacity as u64 {
210 return Err(FenceClockError::LayoutMismatch);
211 }
212 Ok(Self {
213 _file: file, mmap, capacity: expected_capacity,
214 header_sidecar: subetha_core::HandshakeHeader::new(),
215 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
216 })
217 }
218
219 #[inline]
220 pub fn capacity(&self) -> usize { self.capacity }
221
222 #[inline]
226 pub fn shared_clock_us(&self) -> u64 {
227 self.header().cached_us.load(Ordering::Acquire)
228 }
229
230 fn header(&self) -> &HlcHeader {
231 unsafe { &*(self.mmap.as_ptr() as *const HlcHeader) }
232 }
233
234 #[inline]
243 fn shared_now_us(&self) -> u64 {
244 let local = crate::cached_clock::now_us();
245 let cached = &self.header().cached_us;
246 let shared = cached.load(Ordering::Acquire);
247 if local > shared {
248 cached.fetch_max(local, Ordering::AcqRel);
249 local
250 } else {
251 shared
252 }
253 }
254
255 fn slot(&self, idx: usize) -> &HlcSlot {
256 assert!(idx < self.capacity, "slot index {idx} out of range {}", self.capacity);
257 let base = unsafe {
258 self.mmap.as_ptr().add(std::mem::size_of::<HlcHeader>())
259 };
260 unsafe {
261 &*(base.add(idx * std::mem::size_of::<HlcSlot>()) as *const HlcSlot)
262 }
263 }
264
265 pub fn register(&self, pid: u32) -> Result<usize, FenceClockError> {
268 assert!(pid != EMPTY_PID, "pid must be != EMPTY_PID");
269 for i in 0..self.capacity {
270 let slot = self.slot(i);
271 if slot.pid.compare_exchange(
272 EMPTY_PID, pid, Ordering::AcqRel, Ordering::Acquire,
273 ).is_ok() {
274 slot.physical_us.store(0, Ordering::Release);
275 slot.logical.store(0, Ordering::Release);
276 slot.last_updated_us.store(self.shared_now_us(), Ordering::Release);
277 return Ok(i);
278 }
279 }
280 Err(FenceClockError::Full)
281 }
282
283 pub fn unregister(&self, idx: usize) {
285 if idx >= self.capacity { return; }
286 let slot = self.slot(idx);
287 slot.physical_us.store(0, Ordering::Release);
288 slot.logical.store(0, Ordering::Release);
289 slot.pid.store(EMPTY_PID, Ordering::Release);
290 }
291
292 pub fn tick(&self, idx: usize) -> Hlc {
295 let slot = self.slot(idx);
296 let wall = self.shared_now_us();
297 let prev_phys = slot.physical_us.load(Ordering::Acquire);
298 let prev_log = slot.logical.load(Ordering::Acquire);
299 let new_phys = prev_phys.max(wall);
300 let new_log = if new_phys == prev_phys { prev_log + 1 } else { 0 };
301 slot.physical_us.store(new_phys, Ordering::Release);
302 slot.logical.store(new_log, Ordering::Release);
303 slot.last_updated_us.store(wall, Ordering::Release);
304 self.ring_sidecar
305 .push_op(crate::sidecar_ops::fence_clock::OP_TICK, 0);
306 Hlc { physical_us: new_phys, logical: new_log }
307 }
308
309 pub fn merge(&self, idx: usize, remote: Hlc) -> Hlc {
312 let slot = self.slot(idx);
313 let wall = self.shared_now_us();
314 let prev_phys = slot.physical_us.load(Ordering::Acquire);
315 let prev_log = slot.logical.load(Ordering::Acquire);
316 let new_phys = prev_phys.max(remote.physical_us).max(wall);
317 let new_log = if new_phys == prev_phys && new_phys == remote.physical_us {
318 prev_log.max(remote.logical) + 1
319 } else if new_phys == prev_phys {
320 prev_log + 1
321 } else if new_phys == remote.physical_us {
322 remote.logical + 1
323 } else {
324 0
325 };
326 slot.physical_us.store(new_phys, Ordering::Release);
327 slot.logical.store(new_log, Ordering::Release);
328 slot.last_updated_us.store(wall, Ordering::Release);
329 self.ring_sidecar
330 .push_op(crate::sidecar_ops::fence_clock::OP_MERGE, 0);
331 Hlc { physical_us: new_phys, logical: new_log }
332 }
333
334 pub fn get_local(&self, idx: usize) -> Hlc {
336 let slot = self.slot(idx);
337 let h = Hlc {
338 physical_us: slot.physical_us.load(Ordering::Acquire),
339 logical: slot.logical.load(Ordering::Acquire),
340 };
341 self.ring_sidecar
342 .push_op(crate::sidecar_ops::fence_clock::OP_GET_LOCAL, 0);
343 h
344 }
345
346 pub fn compute_global_fence(&self) -> Hlc {
350 let mut max = Hlc { physical_us: 0, logical: 0 };
351 for i in 0..self.capacity {
352 let slot = self.slot(i);
353 if slot.pid.load(Ordering::Acquire) == EMPTY_PID { continue; }
354 let h = Hlc {
355 physical_us: slot.physical_us.load(Ordering::Acquire),
356 logical: slot.logical.load(Ordering::Acquire),
357 };
358 if h > max { max = h; }
359 }
360 self.ring_sidecar
361 .push_op(crate::sidecar_ops::fence_clock::OP_COMPUTE_FENCE, 0);
362 max
363 }
364
365 pub fn publish_global_fence(&self) -> Hlc {
368 let fence = self.compute_global_fence();
369 let hdr = self.header();
370 hdr.global_fence_physical.store(fence.physical_us, Ordering::Release);
371 hdr.global_fence_logical.store(fence.logical, Ordering::Release);
372 hdr.last_fence_epoch.fetch_add(1, Ordering::Release);
373 fence
374 }
375
376 pub fn read_global_fence(&self) -> Hlc {
379 let hdr = self.header();
380 Hlc {
381 physical_us: hdr.global_fence_physical.load(Ordering::Acquire),
382 logical: hdr.global_fence_logical.load(Ordering::Acquire),
383 }
384 }
385
386 pub fn slot_snapshot(&self, idx: usize) -> Option<HlcSlotSnapshot> {
388 if idx >= self.capacity { return None; }
389 let slot = self.slot(idx);
390 let pid = slot.pid.load(Ordering::Acquire);
391 if pid == EMPTY_PID { return None; }
392 Some(HlcSlotSnapshot {
393 pid,
394 hlc: Hlc {
395 physical_us: slot.physical_us.load(Ordering::Acquire),
396 logical: slot.logical.load(Ordering::Acquire),
397 },
398 last_updated_us: slot.last_updated_us.load(Ordering::Acquire),
399 })
400 }
401
402 pub fn fence_epoch(&self) -> u64 {
404 self.header().last_fence_epoch.load(Ordering::Acquire)
405 }
406
407 pub fn flush(&self) -> Result<(), FenceClockError> {
408 self.mmap.flush()?;
409 Ok(())
410 }
411
412 pub fn flush_async(&self) -> Result<(), FenceClockError> {
416 self.mmap.flush_async()?;
417 Ok(())
418 }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub struct HlcSlotSnapshot {
423 pub pid: u32,
424 pub hlc: Hlc,
425 pub last_updated_us: u64,
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use std::sync::Arc;
432 use std::thread;
433
434 fn tmp(name: &str) -> std::path::PathBuf {
435 let mut p = std::env::temp_dir();
436 let pid = std::process::id();
437 p.push(format!("subetha-fenceclock-{name}-{pid}.bin"));
438 p
439 }
440
441 #[test]
442 fn create_initial_state_is_empty() {
443 let p = tmp("init");
444 let c = SharedFenceClock::create(&p, 4).unwrap();
445 assert_eq!(c.capacity(), 4);
446 assert_eq!(c.compute_global_fence(), Hlc { physical_us: 0, logical: 0 });
447 for i in 0..4 { assert!(c.slot_snapshot(i).is_none()); }
448 std::fs::remove_file(&p).ok();
449 }
450
451 #[test]
452 fn register_returns_distinct_slot_indices() {
453 let p = tmp("reg");
454 let c = SharedFenceClock::create(&p, 4).unwrap();
455 let s0 = c.register(1001).unwrap();
456 let s1 = c.register(1002).unwrap();
457 assert_ne!(s0, s1);
458 std::fs::remove_file(&p).ok();
459 }
460
461 #[test]
462 fn register_fails_when_table_is_full() {
463 let p = tmp("full");
464 let c = SharedFenceClock::create(&p, 2).unwrap();
465 c.register(1).unwrap();
466 c.register(2).unwrap();
467 assert_eq!(c.register(3).err(), Some(FenceClockError::Full));
468 std::fs::remove_file(&p).ok();
469 }
470
471 #[test]
472 fn tick_advances_physical_and_resets_logical() {
473 let p = tmp("tick");
474 let c = SharedFenceClock::create(&p, 2).unwrap();
475 let idx = c.register(1).unwrap();
476 let h1 = c.tick(idx);
477 thread::sleep(std::time::Duration::from_millis(2));
478 let h2 = c.tick(idx);
479 assert!(h2 > h1, "second tick should be strictly greater");
480 std::fs::remove_file(&p).ok();
481 }
482
483 #[test]
484 fn tick_increments_logical_when_physical_unchanged() {
485 let p = tmp("logical");
486 let c = SharedFenceClock::create(&p, 2).unwrap();
487 let idx = c.register(1).unwrap();
488 let slot = c.slot(idx);
492 slot.physical_us.store(u64::MAX / 2, Ordering::Release);
493 slot.logical.store(0, Ordering::Release);
494 let h0 = c.tick(idx);
495 let h1 = c.tick(idx);
496 assert_eq!(h0.physical_us, h1.physical_us);
497 assert_eq!(h1.logical, h0.logical + 1);
498 std::fs::remove_file(&p).ok();
499 }
500
501 #[test]
502 fn merge_picks_max_physical_and_increments_logical() {
503 let p = tmp("merge");
504 let c = SharedFenceClock::create(&p, 2).unwrap();
505 let idx = c.register(1).unwrap();
506 let slot = c.slot(idx);
508 slot.physical_us.store(1000, Ordering::Release);
509 slot.logical.store(5, Ordering::Release);
510 let remote = Hlc { physical_us: 2000, logical: 3 };
512 let remote_far_future = Hlc { physical_us: u64::MAX / 2, logical: 7 };
516 let merged = c.merge(idx, remote_far_future);
517 assert_eq!(merged.physical_us, u64::MAX / 2);
518 assert_eq!(merged.logical, 8);
519 let _remote = remote;
522 std::fs::remove_file(&p).ok();
523 }
524
525 #[test]
526 fn compute_global_fence_returns_max_of_all_slots() {
527 let p = tmp("global");
528 let c = SharedFenceClock::create(&p, 4).unwrap();
529 let a = c.register(10).unwrap();
530 let b = c.register(20).unwrap();
531 let d = c.register(30).unwrap();
532 c.slot(a).physical_us.store(100, Ordering::Release);
534 c.slot(a).logical.store(5, Ordering::Release);
535 c.slot(b).physical_us.store(200, Ordering::Release);
536 c.slot(b).logical.store(0, Ordering::Release);
537 c.slot(d).physical_us.store(150, Ordering::Release);
538 c.slot(d).logical.store(9, Ordering::Release);
539 let fence = c.compute_global_fence();
540 assert_eq!(fence, Hlc { physical_us: 200, logical: 0 });
541 std::fs::remove_file(&p).ok();
542 }
543
544 #[test]
545 fn publish_and_read_global_fence_round_trip() {
546 let p = tmp("publish");
547 let c = SharedFenceClock::create(&p, 2).unwrap();
548 let idx = c.register(1).unwrap();
549 c.slot(idx).physical_us.store(7777, Ordering::Release);
550 c.slot(idx).logical.store(3, Ordering::Release);
551 let published = c.publish_global_fence();
552 let read = c.read_global_fence();
553 assert_eq!(read, published);
554 assert_eq!(read, Hlc { physical_us: 7777, logical: 3 });
555 assert_eq!(c.fence_epoch(), 1);
556 std::fs::remove_file(&p).ok();
557 }
558
559 #[test]
560 fn cross_handle_fence_visible() {
561 let p = tmp("cross-handle");
562 let owner = SharedFenceClock::create(&p, 4).unwrap();
563 let observer = SharedFenceClock::open(&p, 4).unwrap();
564 let idx = owner.register(42).unwrap();
565 owner.slot(idx).physical_us.store(5555, Ordering::Release);
566 owner.slot(idx).logical.store(1, Ordering::Release);
567 let owner_fence = owner.publish_global_fence();
568 let observer_fence = observer.read_global_fence();
569 assert_eq!(owner_fence, observer_fence);
570 assert_eq!(observer.compute_global_fence(), Hlc { physical_us: 5555, logical: 1 });
572 std::fs::remove_file(&p).ok();
573 }
574
575 #[test]
576 fn concurrent_ticks_remain_monotonic() {
577 let p = tmp("monotonic");
578 let c = Arc::new(SharedFenceClock::create(&p, 8).unwrap());
579 let mut handles = vec![];
580 for t in 0..4 {
581 let c = c.clone();
582 handles.push(thread::spawn(move || {
583 let idx = c.register(1000 + t as u32).unwrap();
584 let mut prev = Hlc { physical_us: 0, logical: 0 };
585 for _ in 0..50 {
586 let cur = c.tick(idx);
587 assert!(cur > prev, "tick must produce strictly greater HLC");
588 prev = cur;
589 }
590 }));
591 }
592 for h in handles { h.join().unwrap(); }
593 let fence = c.compute_global_fence();
594 assert!(fence.physical_us > 0);
595 std::fs::remove_file(&p).ok();
596 }
597
598 #[test]
599 fn unregister_clears_slot() {
600 let p = tmp("unreg");
601 let c = SharedFenceClock::create(&p, 4).unwrap();
602 let idx = c.register(999).unwrap();
603 c.tick(idx);
604 assert!(c.slot_snapshot(idx).is_some());
605 c.unregister(idx);
606 assert!(c.slot_snapshot(idx).is_none());
607 let idx2 = c.register(1000).unwrap();
609 assert_eq!(idx, idx2);
610 std::fs::remove_file(&p).ok();
611 }
612
613 #[test]
614 fn disk_persistence_fence_survives_reopen() {
615 let p = tmp("disk");
616 {
617 let c = SharedFenceClock::create(&p, 4).unwrap();
618 let idx = c.register(100).unwrap();
619 c.slot(idx).physical_us.store(1234, Ordering::Release);
620 c.slot(idx).logical.store(7, Ordering::Release);
621 c.publish_global_fence();
622 c.flush().unwrap();
623 }
624 let c2 = SharedFenceClock::open(&p, 4).unwrap();
625 assert_eq!(c2.read_global_fence(), Hlc { physical_us: 1234, logical: 7 });
626 std::fs::remove_file(&p).ok();
627 }
628
629 #[test]
630 fn hlc_ord_is_lexicographic() {
631 let a = Hlc { physical_us: 100, logical: 5 };
632 let b = Hlc { physical_us: 100, logical: 6 };
633 let c = Hlc { physical_us: 101, logical: 0 };
634 assert!(a < b);
635 assert!(b < c);
636 assert!(a < c);
637 assert_eq!(a.max(c), c);
638 }
639}