#![deny(missing_docs)]
#![allow(unsafe_op_in_unsafe_fn)]
use frozen_core::{
crc32::Crc32C,
error::{ErrCode, FrozenErr},
fmmap::{FMCfg, FrozenMMap},
hints,
};
use std::{
slice,
sync::{self, atomic},
thread, time,
};
pub use rta_derive::RTA;
const ERRDOMAIN: u8 = 0x14;
const COPIES_ON_DISK: usize = 4;
const MMAP_FLUSH_DURATION: time::Duration = time::Duration::from_secs(1);
const MMAP_CONFIG: FMCfg = FMCfg {
initial_count: COPIES_ON_DISK,
flush_duration: MMAP_FLUSH_DURATION,
};
static MODULE_ID: sync::OnceLock<u8> = sync::OnceLock::new();
static CRC32: sync::OnceLock<Crc32C> = sync::OnceLock::new();
#[inline(always)]
fn mod_id() -> &'static u8 {
MODULE_ID.get().expect("MID OnceLock is not initialized")
}
#[inline(always)]
fn crc32() -> &'static Crc32C {
CRC32.get().expect("CRC32 OnceLock is not initialized")
}
mod err {
use super::ErrCode;
pub const CRP: ErrCode = ErrCode::new(0x500, "All copies of `T` are corrupted");
pub const HSH: ErrCode = ErrCode::new(0x501, "`T` has HASH mismatch as it may be updated after being stored");
pub const DRP: ErrCode = ErrCode::new(0x502, "T must not implement Drop");
pub const ALN: ErrCode = ErrCode::new(0x503, "T must be 8-byte aligned");
pub const ZRO: ErrCode = ErrCode::new(0x504, "T must not be zero-sized");
pub const SZE: ErrCode = ErrCode::new(0x505, "T size must be multiple of 8");
pub const LPN: ErrCode = ErrCode::new(0x506, "lock poisoned internally");
}
#[inline]
fn new_err<R>(code: ErrCode) -> RtaRes<R> {
let err = FrozenErr::new(*mod_id(), ERRDOMAIN, code, "");
Err(err.into())
}
#[inline]
fn new_err_raw<E: std::fmt::Display>(code: ErrCode, error: E) -> RtaErr {
let err = FrozenErr::new_raw(*mod_id(), ERRDOMAIN, code, error);
err.into()
}
pub unsafe trait RTA: Sized + Default {
const HASH: u64;
const SIZE: usize;
}
pub type RtaRes<T> = Result<T, RtaErr>;
#[derive(Debug, Clone)]
pub struct RtaErr {
pub id: u32,
pub context: String,
}
impl From<FrozenErr> for RtaErr {
fn from(value: FrozenErr) -> Self {
Self {
id: value.id,
context: value.context,
}
}
}
pub struct Rta<T: RTA + Send + Sync + Clone, const MOD_ID: u8> {
core: sync::Arc<Core<T, MOD_ID>>,
handle: Option<thread::JoinHandle<()>>,
}
unsafe impl<T, const MOD_ID: u8> Send for Rta<T, MOD_ID> where T: RTA + Default + Send + Sync + Clone + 'static {}
unsafe impl<T, const MOD_ID: u8> Sync for Rta<T, MOD_ID> where T: RTA + Default + Send + Sync + Clone + 'static {}
impl<T, const MOD_ID: u8> Rta<T, MOD_ID>
where
T: RTA + Default + Send + Sync + Clone + 'static,
{
pub fn new<P: AsRef<std::path::Path>>(path: P) -> RtaRes<Self> {
let _ = MODULE_ID.get_or_init(|| MOD_ID);
let _ = CRC32.get_or_init(|| Crc32C::default());
validate_t::<T>()?;
let mmap = FrozenMMap::<DiskObject<T>, MOD_ID>::new(path, MMAP_CONFIG)?;
let (obj, version) = Self::init_or_create(&mmap)?;
let cache = MemCache::new(obj, version);
let core = Core::new(cache, mmap);
let handle = Some(Core::spawn_flush_tx(core.clone()));
Ok(Self { handle, core })
}
#[inline(always)]
pub fn write(&self, f: impl FnOnce(&mut T)) -> RtaRes<()> {
if let Some(err) = self.core.get_sync_error() {
return Err(err);
}
let mut guard = match self.core.guard.lock() {
Ok(g) => g,
Err(e) => {
return Err(new_err_raw(err::LPN, e));
}
};
let mut write_lock = match self.core.cache.write() {
Ok(cache) => cache,
Err(e) => {
return Err(new_err_raw(err::LPN, e));
}
};
f(&mut write_lock.obj);
write_lock.version = write_lock.version.wrapping_add(1);
if !(*guard) {
(*guard) = true;
self.core.cv.notify_one();
}
Ok(())
}
#[inline(always)]
pub fn read(&self) -> RtaRes<T> {
if let Some(err) = self.core.get_sync_error() {
return Err(err);
}
let guard = match self.core.cache.read() {
Ok(g) => g,
Err(e) => {
return Err(new_err_raw(err::LPN, e));
}
};
Ok(guard.obj.clone())
}
fn init_or_create(mmap: &FrozenMMap<DiskObject<T>, MOD_ID>) -> RtaRes<(T, u32)> {
let mut seen_any = false;
let mut seen_compatible = false;
let mut best: Option<DiskObject<T>> = None;
for i in 0..COPIES_ON_DISK {
unsafe {
mmap.read(i, |disk_object| {
let di = &*disk_object;
if di.hsh != 0 {
seen_any = true;
}
if !di.iseq_hsh(T::HASH) {
return;
}
seen_compatible = true;
let crc = crc32().crc(to_bytes(&di.obj));
if di.iseq_crc(crc) {
match &best {
Some(curr) if curr.ver >= di.ver => {}
_ => best = Some(di.clone()),
}
}
})
}?;
}
if let Some(b) = best {
return Ok((b.obj, b.ver));
}
if seen_any && !seen_compatible {
return new_err(err::HSH);
}
if seen_any {
return new_err(err::CRP);
}
let def = T::default();
let crc = crc32().crc(to_bytes(&def));
let mut tx = mmap.new_tx();
for i in 0..COPIES_ON_DISK {
let idx = i;
let ver = if idx == 0 { 1 } else { 0 };
let obj = def.clone();
unsafe {
tx.write(idx, move |disk_object| {
let di = &mut (*disk_object);
di.ver = ver;
di.crc = crc;
di.hsh = T::HASH;
di.obj = obj;
})
}?;
}
let _ = tx.commit()?;
Ok((def, 1))
}
}
impl<T, const MOD_ID: u8> Drop for Rta<T, MOD_ID>
where
T: RTA + Default + Send + Sync + Clone,
{
fn drop(&mut self) {
self.core.shutdown.store(true, atomic::Ordering::Release);
self.core.cv.notify_all();
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
struct Core<T: RTA + Send + Sync + Clone, const MOD_ID: u8> {
cv: sync::Condvar,
guard: sync::Mutex<bool>,
shutdown: atomic::AtomicBool,
cache: sync::RwLock<MemCache<T>>,
mmap: FrozenMMap<DiskObject<T>, MOD_ID>,
error: atomic::AtomicPtr<sync::Arc<RtaErr>>,
}
impl<T, const MOD_ID: u8> Core<T, MOD_ID>
where
T: RTA + Default + Send + Sync + Clone + 'static,
{
fn new(cache: MemCache<T>, mmap: FrozenMMap<DiskObject<T>, MOD_ID>) -> sync::Arc<Self> {
sync::Arc::new(Self {
mmap,
cv: sync::Condvar::new(),
guard: sync::Mutex::new(false),
cache: sync::RwLock::new(cache),
shutdown: atomic::AtomicBool::new(false),
error: atomic::AtomicPtr::new(std::ptr::null_mut()),
})
}
fn spawn_flush_tx(core: sync::Arc<Core<T, MOD_ID>>) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
let mut idx = 0;
loop {
let mut guard = match core.guard.lock() {
Ok(g) => g,
Err(e) => {
core.set_sync_error(new_err_raw(err::LPN, e));
return;
}
};
while !(*guard) && !core.shutdown.load(atomic::Ordering::Acquire) {
guard = match core.cv.wait(guard) {
Ok(g) => g,
Err(e) => {
core.set_sync_error(new_err_raw(err::LPN, e));
return;
}
}
}
let write_lock = match core.cache.write() {
Ok(cache) => cache,
Err(e) => {
core.set_sync_error(new_err_raw(err::LPN, e));
return;
}
};
if core.shutdown.load(atomic::Ordering::Acquire) && !(*guard) {
return;
}
let ver = write_lock.version;
let obj = write_lock.obj.clone();
let crc = crc32().crc(to_bytes(&obj));
drop(write_lock);
match unsafe {
core.mmap.write_sync(idx % COPIES_ON_DISK, |disk_object| {
let di = &mut (*disk_object);
di.obj = obj;
di.ver = ver;
di.crc = crc;
di.hsh = T::HASH;
})
} {
Ok(()) => {
(*guard) = false;
core.clear_sync_error();
}
Err(e) => {
core.set_sync_error(e.into());
}
}
idx = idx.wrapping_add(1);
}
})
}
#[inline(always)]
fn set_sync_error(&self, err: RtaErr) {
let boxed = Box::into_raw(Box::new(sync::Arc::new(err)));
let old = self.error.swap(boxed, atomic::Ordering::AcqRel);
if !old.is_null() {
unsafe { drop(Box::from_raw(old)) };
}
}
#[inline(always)]
fn get_sync_error(&self) -> Option<RtaErr> {
let ptr = self.error.load(atomic::Ordering::Acquire);
if hints::likely(ptr.is_null()) {
return None;
}
let arc = unsafe { &*ptr }.clone();
Some((*arc).clone())
}
#[inline]
fn clear_sync_error(&self) {
let old = self.error.swap(std::ptr::null_mut(), atomic::Ordering::AcqRel);
if hints::unlikely(!old.is_null()) {
unsafe {
drop(Box::from_raw(old));
}
}
}
}
struct MemCache<T: RTA> {
obj: T,
version: u32,
}
impl<T: RTA> MemCache<T> {
#[inline]
fn new(obj: T, version: u32) -> Self {
Self { obj, version }
}
}
#[repr(C)]
#[derive(Clone, Copy)]
struct DiskObject<T: RTA> {
ver: u32,
crc: u32,
hsh: u64,
obj: T,
}
impl<T: RTA> DiskObject<T> {
#[inline]
fn iseq_crc(&self, crc: u32) -> bool {
self.crc == crc
}
#[inline]
fn iseq_hsh(&self, hsh: u64) -> bool {
self.hsh == hsh
}
}
#[inline]
fn to_bytes<T: RTA>(t: &T) -> &[u8] {
unsafe { slice::from_raw_parts(t as *const T as *const u8, T::SIZE) }
}
#[inline(always)]
fn validate_t<T: RTA>() -> RtaRes<()> {
if std::mem::needs_drop::<T>() {
return new_err(err::DRP);
}
let align = std::mem::align_of::<T>();
if align != 8 {
return new_err(err::ALN);
}
let size = std::mem::size_of::<T>();
if size == 0 {
return new_err(err::ZRO);
}
if size % 8 != 0 {
return new_err(err::SZE);
}
Ok(())
}