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> {
159 assert!(capacity >= 1);
160 crate::cached_clock::start();
163 let total = fence_clock_file_size(capacity);
164 let (file, mmap) = crate::mmf_attach::create_or_attach(
165 path.as_ref(),
166 total,
167 |ptr| unsafe { Self::init_region(ptr, capacity) },
168 |ptr| unsafe { (*(ptr as *const HlcHeader)).magic == FENCE_CLOCK_MAGIC },
169 )?;
170 Self::from_region(file, mmap, capacity)
171 }
172
173 pub fn reset(path: impl AsRef<Path>, capacity: usize) -> Result<Self, FenceClockError> {
177 assert!(capacity >= 1);
178 crate::cached_clock::start();
179 let total = fence_clock_file_size(capacity);
180 let (file, mmap) = crate::mmf_attach::reset(path.as_ref(), total, |ptr| unsafe {
181 Self::init_region(ptr, capacity)
182 })?;
183 Self::from_region(file, mmap, capacity)
184 }
185
186 unsafe fn init_region(ptr: *mut u8, capacity: usize) {
195 let hdr = ptr as *mut HlcHeader;
196 unsafe {
197 (*hdr).capacity = capacity as u64;
198 std::ptr::write_volatile(&raw mut (*hdr).magic, FENCE_CLOCK_MAGIC);
199 }
200 }
201
202 fn from_region(
205 file: File,
206 mmap: MmapMut,
207 capacity: usize,
208 ) -> Result<Self, FenceClockError> {
209 let hdr = unsafe { &*(mmap.as_ptr() as *const HlcHeader) };
210 if hdr.magic != FENCE_CLOCK_MAGIC || hdr.capacity != capacity as u64 {
211 return Err(FenceClockError::LayoutMismatch);
212 }
213 Ok(Self {
214 _file: file, mmap, capacity,
215 header_sidecar: subetha_core::HandshakeHeader::new(),
216 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
217 })
218 }
219
220 pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> Result<Self, FenceClockError> {
221 crate::cached_clock::start();
222 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
223 let total = fence_clock_file_size(expected_capacity);
224 if file.metadata()?.len() < total as u64 {
225 return Err(FenceClockError::LayoutMismatch);
226 }
227 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
228 Self::from_region(file, mmap, expected_capacity)
229 }
230
231 #[inline]
232 pub fn capacity(&self) -> usize { self.capacity }
233
234 #[inline]
238 pub fn shared_clock_us(&self) -> u64 {
239 self.header().cached_us.load(Ordering::Acquire)
240 }
241
242 fn header(&self) -> &HlcHeader {
243 unsafe { &*(self.mmap.as_ptr() as *const HlcHeader) }
244 }
245
246 #[inline]
255 fn shared_now_us(&self) -> u64 {
256 let local = crate::cached_clock::now_us();
257 let cached = &self.header().cached_us;
258 let shared = cached.load(Ordering::Acquire);
259 if local > shared {
260 cached.fetch_max(local, Ordering::AcqRel);
261 local
262 } else {
263 shared
264 }
265 }
266
267 fn slot(&self, idx: usize) -> &HlcSlot {
268 assert!(idx < self.capacity, "slot index {idx} out of range {}", self.capacity);
269 let base = unsafe {
270 self.mmap.as_ptr().add(std::mem::size_of::<HlcHeader>())
271 };
272 unsafe {
273 &*(base.add(idx * std::mem::size_of::<HlcSlot>()) as *const HlcSlot)
274 }
275 }
276
277 pub fn register(&self, pid: u32) -> Result<usize, FenceClockError> {
280 assert!(pid != EMPTY_PID, "pid must be != EMPTY_PID");
281 for i in 0..self.capacity {
282 let slot = self.slot(i);
283 if slot.pid.compare_exchange(
284 EMPTY_PID, pid, Ordering::AcqRel, Ordering::Acquire,
285 ).is_ok() {
286 slot.physical_us.store(0, Ordering::Release);
287 slot.logical.store(0, Ordering::Release);
288 slot.last_updated_us.store(self.shared_now_us(), Ordering::Release);
289 return Ok(i);
290 }
291 }
292 Err(FenceClockError::Full)
293 }
294
295 pub fn unregister(&self, idx: usize) {
297 if idx >= self.capacity { return; }
298 let slot = self.slot(idx);
299 slot.physical_us.store(0, Ordering::Release);
300 slot.logical.store(0, Ordering::Release);
301 slot.pid.store(EMPTY_PID, Ordering::Release);
302 }
303
304 pub fn tick(&self, idx: usize) -> Hlc {
307 let slot = self.slot(idx);
308 let wall = self.shared_now_us();
309 let prev_phys = slot.physical_us.load(Ordering::Acquire);
310 let prev_log = slot.logical.load(Ordering::Acquire);
311 let new_phys = prev_phys.max(wall);
312 let new_log = if new_phys == prev_phys { prev_log + 1 } else { 0 };
313 slot.physical_us.store(new_phys, Ordering::Release);
314 slot.logical.store(new_log, Ordering::Release);
315 slot.last_updated_us.store(wall, Ordering::Release);
316 self.ring_sidecar
317 .push_op(crate::sidecar_ops::fence_clock::OP_TICK, 0);
318 Hlc { physical_us: new_phys, logical: new_log }
319 }
320
321 pub fn merge(&self, idx: usize, remote: Hlc) -> Hlc {
324 let slot = self.slot(idx);
325 let wall = self.shared_now_us();
326 let prev_phys = slot.physical_us.load(Ordering::Acquire);
327 let prev_log = slot.logical.load(Ordering::Acquire);
328 let new_phys = prev_phys.max(remote.physical_us).max(wall);
329 let new_log = if new_phys == prev_phys && new_phys == remote.physical_us {
330 prev_log.max(remote.logical) + 1
331 } else if new_phys == prev_phys {
332 prev_log + 1
333 } else if new_phys == remote.physical_us {
334 remote.logical + 1
335 } else {
336 0
337 };
338 slot.physical_us.store(new_phys, Ordering::Release);
339 slot.logical.store(new_log, Ordering::Release);
340 slot.last_updated_us.store(wall, Ordering::Release);
341 self.ring_sidecar
342 .push_op(crate::sidecar_ops::fence_clock::OP_MERGE, 0);
343 Hlc { physical_us: new_phys, logical: new_log }
344 }
345
346 pub fn get_local(&self, idx: usize) -> Hlc {
348 let slot = self.slot(idx);
349 let h = Hlc {
350 physical_us: slot.physical_us.load(Ordering::Acquire),
351 logical: slot.logical.load(Ordering::Acquire),
352 };
353 self.ring_sidecar
354 .push_op(crate::sidecar_ops::fence_clock::OP_GET_LOCAL, 0);
355 h
356 }
357
358 pub fn compute_global_fence(&self) -> Hlc {
362 let mut max = Hlc { physical_us: 0, logical: 0 };
363 for i in 0..self.capacity {
364 let slot = self.slot(i);
365 if slot.pid.load(Ordering::Acquire) == EMPTY_PID { continue; }
366 let h = Hlc {
367 physical_us: slot.physical_us.load(Ordering::Acquire),
368 logical: slot.logical.load(Ordering::Acquire),
369 };
370 if h > max { max = h; }
371 }
372 self.ring_sidecar
373 .push_op(crate::sidecar_ops::fence_clock::OP_COMPUTE_FENCE, 0);
374 max
375 }
376
377 pub fn publish_global_fence(&self) -> Hlc {
380 let fence = self.compute_global_fence();
381 let hdr = self.header();
382 hdr.global_fence_physical.store(fence.physical_us, Ordering::Release);
383 hdr.global_fence_logical.store(fence.logical, Ordering::Release);
384 hdr.last_fence_epoch.fetch_add(1, Ordering::Release);
385 fence
386 }
387
388 pub fn read_global_fence(&self) -> Hlc {
391 let hdr = self.header();
392 Hlc {
393 physical_us: hdr.global_fence_physical.load(Ordering::Acquire),
394 logical: hdr.global_fence_logical.load(Ordering::Acquire),
395 }
396 }
397
398 pub fn slot_snapshot(&self, idx: usize) -> Option<HlcSlotSnapshot> {
400 if idx >= self.capacity { return None; }
401 let slot = self.slot(idx);
402 let pid = slot.pid.load(Ordering::Acquire);
403 if pid == EMPTY_PID { return None; }
404 Some(HlcSlotSnapshot {
405 pid,
406 hlc: Hlc {
407 physical_us: slot.physical_us.load(Ordering::Acquire),
408 logical: slot.logical.load(Ordering::Acquire),
409 },
410 last_updated_us: slot.last_updated_us.load(Ordering::Acquire),
411 })
412 }
413
414 pub fn fence_epoch(&self) -> u64 {
416 self.header().last_fence_epoch.load(Ordering::Acquire)
417 }
418
419 pub fn flush(&self) -> Result<(), FenceClockError> {
420 self.mmap.flush()?;
421 Ok(())
422 }
423
424 pub fn flush_async(&self) -> Result<(), FenceClockError> {
428 self.mmap.flush_async()?;
429 Ok(())
430 }
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
434pub struct HlcSlotSnapshot {
435 pub pid: u32,
436 pub hlc: Hlc,
437 pub last_updated_us: u64,
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443 use std::sync::Arc;
444 use std::thread;
445
446 fn tmp(name: &str) -> std::path::PathBuf {
447 let mut p = std::env::temp_dir();
448 let pid = std::process::id();
449 p.push(format!("subetha-fenceclock-{name}-{pid}.bin"));
450 p
451 }
452
453 #[test]
454 fn create_initial_state_is_empty() {
455 let p = tmp("init");
456 let c = SharedFenceClock::create(&p, 4).unwrap();
457 assert_eq!(c.capacity(), 4);
458 assert_eq!(c.compute_global_fence(), Hlc { physical_us: 0, logical: 0 });
459 for i in 0..4 { assert!(c.slot_snapshot(i).is_none()); }
460 std::fs::remove_file(&p).ok();
461 }
462
463 #[test]
466 fn second_create_attaches_and_keeps_registrations() {
467 let p = tmp("attach");
468 std::fs::remove_file(&p).ok();
469 let c = SharedFenceClock::create(&p, 4).unwrap();
470 let s0 = c.register(1001).unwrap();
471
472 let c2 = SharedFenceClock::create(&p, 4).unwrap();
473 assert_eq!(
474 c2.slot_snapshot(s0).map(|s| s.pid),
475 Some(1001),
476 "attach dropped a registered slot",
477 );
478 assert!(matches!(
479 SharedFenceClock::create(&p, 2),
480 Err(FenceClockError::LayoutMismatch),
481 ));
482
483 drop(c);
486 drop(c2);
487 let fresh = SharedFenceClock::reset(&p, 4).unwrap();
488 assert!(fresh.slot_snapshot(s0).is_none(), "reset left a slot registered");
489 drop(fresh);
490 std::fs::remove_file(&p).ok();
491 }
492
493 #[test]
494 fn register_returns_distinct_slot_indices() {
495 let p = tmp("reg");
496 let c = SharedFenceClock::create(&p, 4).unwrap();
497 let s0 = c.register(1001).unwrap();
498 let s1 = c.register(1002).unwrap();
499 assert_ne!(s0, s1);
500 std::fs::remove_file(&p).ok();
501 }
502
503 #[test]
504 fn register_fails_when_table_is_full() {
505 let p = tmp("full");
506 let c = SharedFenceClock::create(&p, 2).unwrap();
507 c.register(1).unwrap();
508 c.register(2).unwrap();
509 assert_eq!(c.register(3).err(), Some(FenceClockError::Full));
510 std::fs::remove_file(&p).ok();
511 }
512
513 #[test]
514 fn tick_advances_physical_and_resets_logical() {
515 let p = tmp("tick");
516 let c = SharedFenceClock::create(&p, 2).unwrap();
517 let idx = c.register(1).unwrap();
518 let h1 = c.tick(idx);
519 thread::sleep(std::time::Duration::from_millis(2));
520 let h2 = c.tick(idx);
521 assert!(h2 > h1, "second tick should be strictly greater");
522 std::fs::remove_file(&p).ok();
523 }
524
525 #[test]
526 fn tick_increments_logical_when_physical_unchanged() {
527 let p = tmp("logical");
528 let c = SharedFenceClock::create(&p, 2).unwrap();
529 let idx = c.register(1).unwrap();
530 let slot = c.slot(idx);
534 slot.physical_us.store(u64::MAX / 2, Ordering::Release);
535 slot.logical.store(0, Ordering::Release);
536 let h0 = c.tick(idx);
537 let h1 = c.tick(idx);
538 assert_eq!(h0.physical_us, h1.physical_us);
539 assert_eq!(h1.logical, h0.logical + 1);
540 std::fs::remove_file(&p).ok();
541 }
542
543 #[test]
544 fn merge_picks_max_physical_and_increments_logical() {
545 let p = tmp("merge");
546 let c = SharedFenceClock::create(&p, 2).unwrap();
547 let idx = c.register(1).unwrap();
548 let slot = c.slot(idx);
550 slot.physical_us.store(1000, Ordering::Release);
551 slot.logical.store(5, Ordering::Release);
552 let remote = Hlc { physical_us: 2000, logical: 3 };
554 let remote_far_future = Hlc { physical_us: u64::MAX / 2, logical: 7 };
558 let merged = c.merge(idx, remote_far_future);
559 assert_eq!(merged.physical_us, u64::MAX / 2);
560 assert_eq!(merged.logical, 8);
561 let _remote = remote;
564 std::fs::remove_file(&p).ok();
565 }
566
567 #[test]
568 fn compute_global_fence_returns_max_of_all_slots() {
569 let p = tmp("global");
570 let c = SharedFenceClock::create(&p, 4).unwrap();
571 let a = c.register(10).unwrap();
572 let b = c.register(20).unwrap();
573 let d = c.register(30).unwrap();
574 c.slot(a).physical_us.store(100, Ordering::Release);
576 c.slot(a).logical.store(5, Ordering::Release);
577 c.slot(b).physical_us.store(200, Ordering::Release);
578 c.slot(b).logical.store(0, Ordering::Release);
579 c.slot(d).physical_us.store(150, Ordering::Release);
580 c.slot(d).logical.store(9, Ordering::Release);
581 let fence = c.compute_global_fence();
582 assert_eq!(fence, Hlc { physical_us: 200, logical: 0 });
583 std::fs::remove_file(&p).ok();
584 }
585
586 #[test]
587 fn publish_and_read_global_fence_round_trip() {
588 let p = tmp("publish");
589 let c = SharedFenceClock::create(&p, 2).unwrap();
590 let idx = c.register(1).unwrap();
591 c.slot(idx).physical_us.store(7777, Ordering::Release);
592 c.slot(idx).logical.store(3, Ordering::Release);
593 let published = c.publish_global_fence();
594 let read = c.read_global_fence();
595 assert_eq!(read, published);
596 assert_eq!(read, Hlc { physical_us: 7777, logical: 3 });
597 assert_eq!(c.fence_epoch(), 1);
598 std::fs::remove_file(&p).ok();
599 }
600
601 #[test]
602 fn cross_handle_fence_visible() {
603 let p = tmp("cross-handle");
604 let owner = SharedFenceClock::create(&p, 4).unwrap();
605 let observer = SharedFenceClock::open(&p, 4).unwrap();
606 let idx = owner.register(42).unwrap();
607 owner.slot(idx).physical_us.store(5555, Ordering::Release);
608 owner.slot(idx).logical.store(1, Ordering::Release);
609 let owner_fence = owner.publish_global_fence();
610 let observer_fence = observer.read_global_fence();
611 assert_eq!(owner_fence, observer_fence);
612 assert_eq!(observer.compute_global_fence(), Hlc { physical_us: 5555, logical: 1 });
614 std::fs::remove_file(&p).ok();
615 }
616
617 #[test]
618 fn concurrent_ticks_remain_monotonic() {
619 let p = tmp("monotonic");
620 let c = Arc::new(SharedFenceClock::create(&p, 8).unwrap());
621 let mut handles = vec![];
622 for t in 0..4 {
623 let c = c.clone();
624 handles.push(thread::spawn(move || {
625 let idx = c.register(1000 + t as u32).unwrap();
626 let mut prev = Hlc { physical_us: 0, logical: 0 };
627 for _ in 0..50 {
628 let cur = c.tick(idx);
629 assert!(cur > prev, "tick must produce strictly greater HLC");
630 prev = cur;
631 }
632 }));
633 }
634 for h in handles { h.join().unwrap(); }
635 let fence = c.compute_global_fence();
636 assert!(fence.physical_us > 0);
637 std::fs::remove_file(&p).ok();
638 }
639
640 #[test]
641 fn unregister_clears_slot() {
642 let p = tmp("unreg");
643 let c = SharedFenceClock::create(&p, 4).unwrap();
644 let idx = c.register(999).unwrap();
645 c.tick(idx);
646 assert!(c.slot_snapshot(idx).is_some());
647 c.unregister(idx);
648 assert!(c.slot_snapshot(idx).is_none());
649 let idx2 = c.register(1000).unwrap();
651 assert_eq!(idx, idx2);
652 std::fs::remove_file(&p).ok();
653 }
654
655 #[test]
656 fn disk_persistence_fence_survives_reopen() {
657 let p = tmp("disk");
658 {
659 let c = SharedFenceClock::create(&p, 4).unwrap();
660 let idx = c.register(100).unwrap();
661 c.slot(idx).physical_us.store(1234, Ordering::Release);
662 c.slot(idx).logical.store(7, Ordering::Release);
663 c.publish_global_fence();
664 c.flush().unwrap();
665 }
666 let c2 = SharedFenceClock::open(&p, 4).unwrap();
667 assert_eq!(c2.read_global_fence(), Hlc { physical_us: 1234, logical: 7 });
668 std::fs::remove_file(&p).ok();
669 }
670
671 #[test]
672 fn hlc_ord_is_lexicographic() {
673 let a = Hlc { physical_us: 100, logical: 5 };
674 let b = Hlc { physical_us: 100, logical: 6 };
675 let c = Hlc { physical_us: 101, logical: 0 };
676 assert!(a < b);
677 assert!(b < c);
678 assert!(a < c);
679 assert_eq!(a.max(c), c);
680 }
681}