use std::fs::{File, OpenOptions};
use std::path::Path;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use memmap2::{MmapMut, MmapOptions};
pub const HEARTBEAT_MAGIC: u64 = 0x4150_4D46_4842_4154;
pub const EMPTY_PID: u32 = 0;
pub const IN_FLIGHT_SLOTS: usize = 64;
#[repr(C, align(64))]
pub struct HeartbeatHeader {
pub magic: u64,
pub capacity: u64,
pub epoch: AtomicU64,
_reserved: [u8; 40],
}
#[repr(C, align(64))]
pub struct HeartbeatSlot {
pub pid: AtomicU32,
pub seq_version: AtomicU32,
pub last_seen_epoch: AtomicU64,
pub in_flight_bitmap: AtomicU64,
pub role: AtomicU32,
_pad: [u8; 36],
}
pub const fn heartbeat_file_size(capacity: usize) -> usize {
std::mem::size_of::<HeartbeatHeader>() + capacity * std::mem::size_of::<HeartbeatSlot>()
}
pub struct HeartbeatTable {
_file: File,
mmap: MmapMut,
capacity: usize,
header_sidecar: subetha_core::HandshakeHeader,
ring_sidecar: Box<subetha_core::ObservationRing>,
}
unsafe impl Send for HeartbeatTable {}
unsafe impl Sync for HeartbeatTable {}
impl subetha_sidecar::AdaptiveInstance for HeartbeatTable {
fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
Box::new(subetha_sidecar::NoMigrationPolicy)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeartbeatError {
LayoutMismatch,
TableFull,
IoError(std::io::ErrorKind),
}
impl From<std::io::Error> for HeartbeatError {
fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
}
impl HeartbeatTable {
pub fn create(path: impl AsRef<Path>, capacity: usize) -> Result<Self, HeartbeatError> {
assert!(capacity >= 1);
let total = heartbeat_file_size(capacity);
let file = OpenOptions::new()
.read(true).write(true).create(true).truncate(true)
.open(path.as_ref())?;
file.set_len(total as u64)?;
let mut mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
let hdr_ptr = mmap.as_mut_ptr() as *mut HeartbeatHeader;
unsafe {
std::ptr::write(hdr_ptr, HeartbeatHeader {
magic: HEARTBEAT_MAGIC,
capacity: capacity as u64,
epoch: AtomicU64::new(0),
_reserved: [0; 40],
});
}
let slots_base = unsafe {
mmap.as_mut_ptr().add(std::mem::size_of::<HeartbeatHeader>())
};
for i in 0..capacity {
let slot_ptr = unsafe {
slots_base.add(i * std::mem::size_of::<HeartbeatSlot>()) as *mut HeartbeatSlot
};
unsafe {
std::ptr::write(slot_ptr, HeartbeatSlot {
pid: AtomicU32::new(EMPTY_PID),
seq_version: AtomicU32::new(0),
last_seen_epoch: AtomicU64::new(0),
in_flight_bitmap: AtomicU64::new(0),
role: AtomicU32::new(0),
_pad: [0; 36],
});
}
}
Ok(Self {
_file: file, mmap, capacity,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
pub fn open(path: impl AsRef<Path>, expected_capacity: usize) -> Result<Self, HeartbeatError> {
let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
let total = heartbeat_file_size(expected_capacity);
let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
let header = unsafe { &*(mmap.as_ptr() as *const HeartbeatHeader) };
if header.magic != HEARTBEAT_MAGIC || header.capacity != expected_capacity as u64 {
return Err(HeartbeatError::LayoutMismatch);
}
Ok(Self {
_file: file, mmap, capacity: expected_capacity,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
pub fn capacity(&self) -> usize { self.capacity }
pub fn header(&self) -> &HeartbeatHeader {
unsafe { &*(self.mmap.as_ptr() as *const HeartbeatHeader) }
}
fn slot(&self, idx: usize) -> &HeartbeatSlot {
let base = unsafe {
self.mmap.as_ptr().add(std::mem::size_of::<HeartbeatHeader>())
};
unsafe {
&*(base.add(idx * std::mem::size_of::<HeartbeatSlot>()) as *const HeartbeatSlot)
}
}
pub fn register(&self, pid: u32) -> Result<usize, HeartbeatError> {
for i in 0..self.capacity {
let slot = self.slot(i);
if slot.pid.compare_exchange(
EMPTY_PID, pid, Ordering::AcqRel, Ordering::Acquire,
).is_ok() {
slot.seq_version.fetch_add(1, Ordering::Release);
slot.last_seen_epoch.store(
self.header().epoch.load(Ordering::Acquire),
Ordering::Release,
);
slot.in_flight_bitmap.store(0, Ordering::Release);
slot.role.store(0, Ordering::Release);
slot.seq_version.fetch_add(1, Ordering::Release);
self.ring_sidecar
.push_op(crate::sidecar_ops::liveness::OP_REGISTER, 0);
return Ok(i);
}
}
self.ring_sidecar
.push_op(crate::sidecar_ops::liveness::OP_REGISTER, 1);
Err(HeartbeatError::TableFull)
}
pub fn unregister(&self, idx: usize) {
let slot = self.slot(idx);
slot.seq_version.fetch_add(1, Ordering::Release);
slot.in_flight_bitmap.store(0, Ordering::Release);
slot.pid.store(EMPTY_PID, Ordering::Release);
slot.seq_version.fetch_add(1, Ordering::Release);
}
pub fn beat(&self, idx: usize) {
let global = self.header().epoch.load(Ordering::Acquire);
let slot = self.slot(idx);
slot.last_seen_epoch.store(global, Ordering::Release);
self.ring_sidecar
.push_op(crate::sidecar_ops::liveness::OP_BEAT, 0);
}
pub fn tick_global_epoch(&self) -> u64 {
let v = self.header().epoch.fetch_add(1, Ordering::AcqRel) + 1;
self.ring_sidecar
.push_op(crate::sidecar_ops::liveness::OP_TICK_EPOCH, 0);
v
}
pub fn global_epoch(&self) -> u64 {
self.header().epoch.load(Ordering::Acquire)
}
pub fn mark_in_flight(&self, slot_idx: usize, bit: u8) {
debug_assert!((bit as usize) < IN_FLIGHT_SLOTS);
let slot = self.slot(slot_idx);
slot.in_flight_bitmap.fetch_or(1u64 << bit, Ordering::AcqRel);
}
pub fn clear_in_flight(&self, slot_idx: usize, bit: u8) {
debug_assert!((bit as usize) < IN_FLIGHT_SLOTS);
let slot = self.slot(slot_idx);
slot.in_flight_bitmap.fetch_and(!(1u64 << bit), Ordering::AcqRel);
}
pub fn snapshot(&self, idx: usize) -> Option<HeartbeatSnapshot> {
let slot = self.slot(idx);
loop {
let v1 = slot.seq_version.load(Ordering::Acquire);
if v1 & 1 != 0 { continue; } let pid = slot.pid.load(Ordering::Acquire);
let last = slot.last_seen_epoch.load(Ordering::Acquire);
let inflight = slot.in_flight_bitmap.load(Ordering::Acquire);
let role = slot.role.load(Ordering::Acquire);
let v2 = slot.seq_version.load(Ordering::Acquire);
if v1 == v2 {
if pid == EMPTY_PID { return None; }
return Some(HeartbeatSnapshot {
pid, last_seen_epoch: last,
in_flight_bitmap: inflight, role,
});
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HeartbeatSnapshot {
pub pid: u32,
pub last_seen_epoch: u64,
pub in_flight_bitmap: u64,
pub role: u32,
}
#[doc(hidden)]
pub fn __slot_for_watchdog(table: &HeartbeatTable, idx: usize) -> &HeartbeatSlot {
table.slot(idx)
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_path(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
let pid = std::process::id();
p.push(format!("subetha-hb-{name}-{pid}.bin"));
p
}
#[test]
fn register_returns_slot_indices() {
let p = tmp_path("register");
let t = HeartbeatTable::create(&p, 4).unwrap();
let s0 = t.register(1001).unwrap();
let s1 = t.register(1002).unwrap();
assert_ne!(s0, s1);
std::fs::remove_file(&p).ok();
}
#[test]
fn table_full_returns_error() {
let p = tmp_path("table-full");
let t = HeartbeatTable::create(&p, 2).unwrap();
let _val = t.register(1).unwrap();
let _val = t.register(2).unwrap();
assert_eq!(t.register(3).unwrap_err(), HeartbeatError::TableFull);
std::fs::remove_file(&p).ok();
}
#[test]
fn beat_advances_last_seen_epoch() {
let p = tmp_path("beat");
let t = HeartbeatTable::create(&p, 1).unwrap();
let s = t.register(99).unwrap();
for _ in 0..5 { t.tick_global_epoch(); }
let snap_before = t.snapshot(s).unwrap();
let global_after_tick = t.global_epoch();
t.beat(s);
let snap_after = t.snapshot(s).unwrap();
assert!(snap_after.last_seen_epoch > snap_before.last_seen_epoch);
assert_eq!(snap_after.last_seen_epoch, global_after_tick);
std::fs::remove_file(&p).ok();
}
#[test]
fn unregister_frees_slot_for_reuse() {
let p = tmp_path("unreg");
let t = HeartbeatTable::create(&p, 2).unwrap();
let s0 = t.register(11).unwrap();
let _s1 = t.register(22).unwrap();
t.unregister(s0);
let new = t.register(33).unwrap();
assert_eq!(new, s0);
std::fs::remove_file(&p).ok();
}
#[test]
fn in_flight_bitmap_mark_and_clear() {
let p = tmp_path("inflight");
let t = HeartbeatTable::create(&p, 1).unwrap();
let s = t.register(7).unwrap();
t.mark_in_flight(s, 3);
t.mark_in_flight(s, 5);
let snap = t.snapshot(s).unwrap();
assert_eq!(snap.in_flight_bitmap, (1u64 << 3) | (1u64 << 5));
t.clear_in_flight(s, 3);
let snap = t.snapshot(s).unwrap();
assert_eq!(snap.in_flight_bitmap, 1u64 << 5);
std::fs::remove_file(&p).ok();
}
#[test]
fn snapshot_via_seqlock_returns_consistent_data() {
let p = tmp_path("snap");
let t = HeartbeatTable::create(&p, 1).unwrap();
let s = t.register(42).unwrap();
t.tick_global_epoch();
t.beat(s);
let snap = t.snapshot(s).unwrap();
assert_eq!(snap.pid, 42);
assert!(snap.last_seen_epoch >= 1);
std::fs::remove_file(&p).ok();
}
}