use std::fs::OpenOptions;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use memmap2::MmapMut;
pub const SHM_OFFSET_MAGIC: u64 = 0x53484D5F4F464653;
pub const SHM_OFFSET_VERSION: u32 = 1;
#[repr(C, align(64))]
pub struct ShmOffsetSlot {
pub magic: u64,
pub version: u32,
pub pid: AtomicU32,
pub offset_seq: AtomicU64,
pub updated_ns: AtomicU64,
pub name: [u8; 32],
}
pub struct OffsetCheckpoint {
path: PathBuf,
name: String,
_mmap: MmapMut,
slot: *mut ShmOffsetSlot,
}
unsafe impl Send for OffsetCheckpoint {}
unsafe impl Sync for OffsetCheckpoint {}
#[inline]
fn current_time_nanos() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
}
impl OffsetCheckpoint {
pub fn open_or_create<P: AsRef<Path>>(path: P, consumer_name: &str) -> io::Result<Self> {
let path_buf = path.as_ref().to_path_buf();
if let Some(parent) = path_buf.parent()
&& !parent.as_os_str().is_empty()
{
let _ = std::fs::create_dir_all(parent);
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path_buf)?;
let slot_size = std::mem::size_of::<ShmOffsetSlot>();
let current_len = file.metadata()?.len();
let is_new = if current_len < slot_size as u64 {
file.set_len(slot_size as u64)?;
true
} else {
false
};
let mut mmap = unsafe { MmapMut::map_mut(&file)? };
let slot_ptr = mmap.as_mut_ptr().cast::<ShmOffsetSlot>();
let current_pid = std::process::id();
let name_trimmed = consumer_name.trim();
if is_new {
let mut name_buf = [0u8; 32];
let bytes = name_trimmed.as_bytes();
let len = bytes.len().min(31);
name_buf[..len].copy_from_slice(&bytes[..len]);
unsafe {
slot_ptr.write(ShmOffsetSlot {
magic: SHM_OFFSET_MAGIC,
version: SHM_OFFSET_VERSION,
pid: AtomicU32::new(current_pid),
offset_seq: AtomicU64::new(0),
updated_ns: AtomicU64::new(current_time_nanos()),
name: name_buf,
});
}
} else {
let slot = unsafe { &*slot_ptr };
if slot.magic != SHM_OFFSET_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Invalid magic in shared memory offset {:?}: expected 0x{:016X}, got 0x{:016X}",
path_buf, SHM_OFFSET_MAGIC, slot.magic
),
));
}
if slot.version != SHM_OFFSET_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Version mismatch in shared memory offset {:?}: expected {}, got {}",
path_buf, SHM_OFFSET_VERSION, slot.version
),
));
}
slot.pid.store(current_pid, Ordering::Release);
}
Ok(Self {
path: path_buf,
name: name_trimmed.to_string(),
_mmap: mmap,
slot: slot_ptr,
})
}
pub fn for_consumer<P: AsRef<Path>>(ring_path: P, consumer_name: &str) -> io::Result<Self> {
let p = ring_path.as_ref();
let parent = p.parent().unwrap_or_else(|| Path::new("/dev/shm"));
let stem = p.file_stem().and_then(|s| s.to_str()).unwrap_or("ring");
let safe_name: String = consumer_name
.chars()
.map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' })
.collect();
let offset_path = parent.join(format!("{}_{}.offset", stem, safe_name));
Self::open_or_create(offset_path, consumer_name)
}
#[inline(always)]
pub fn load(&self) -> Option<u64> {
let seq = unsafe { (*self.slot).offset_seq.load(Ordering::Acquire) };
if seq == 0 {
None
} else {
Some(seq)
}
}
#[inline(always)]
pub fn save(&self, seq: u64) {
unsafe {
(*self.slot).offset_seq.store(seq, Ordering::Release);
(*self.slot).updated_ns.store(current_time_nanos(), Ordering::Relaxed);
}
}
#[inline]
pub fn path(&self) -> &Path {
&self.path
}
#[inline]
pub fn name(&self) -> &str {
&self.name
}
#[inline]
pub fn updated_nanos(&self) -> u64 {
unsafe { (*self.slot).updated_ns.load(Ordering::Acquire) }
}
#[inline]
pub fn pid(&self) -> u32 {
unsafe { (*self.slot).pid.load(Ordering::Acquire) }
}
}
impl std::fmt::Debug for OffsetCheckpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OffsetCheckpoint")
.field("path", &self.path)
.field("name", &self.name)
.field("offset_seq", &self.load())
.field("pid", &self.pid())
.field("updated_nanos", &self.updated_nanos())
.finish()
}
}
#[cfg(test)]
#[path = "../tests/unit/checkpoint.rs"]
mod tests;