use std::sync::atomic::{AtomicPtr, AtomicU64, Ordering};
use std::sync::{Arc, Weak};
use crossbeam_epoch::Guard;
use memmap2::Mmap;
use parking_lot::Mutex;
type RetiredList<T> = Mutex<Vec<(u64, Arc<T>)>>;
pub(crate) struct ArcCell<T: Send + Sync + 'static> {
current: AtomicPtr<T>,
retired: Arc<RetiredList<T>>,
next_retire_id: AtomicU64,
}
impl<T: Send + Sync + 'static> std::fmt::Debug for ArcCell<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ArcCell")
.field("retired", &self.retired.lock().len())
.finish_non_exhaustive()
}
}
pub(crate) struct CellRef<'a, T> {
value: &'a T,
}
impl<T> Clone for CellRef<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for CellRef<'_, T> {}
pub(crate) type MmapView<'a> = CellRef<'a, Mmap>;
impl<'a, T> CellRef<'a, T> {
#[inline]
pub(crate) fn get(&self) -> &'a T {
self.value
}
pub(crate) fn to_arc(self) -> Arc<T> {
let ptr: *const T = self.value;
unsafe {
Arc::increment_strong_count(ptr);
Arc::from_raw(ptr)
}
}
}
impl<'a> CellRef<'a, Mmap> {
#[inline]
pub(crate) fn bytes(&self) -> &'a [u8] {
self.value
}
}
impl<T: Send + Sync + 'static> ArcCell<T> {
pub(crate) fn new(value: Arc<T>) -> Self {
Self {
current: AtomicPtr::new(Arc::into_raw(value).cast_mut()),
retired: Arc::new(Mutex::new(Vec::new())),
next_retire_id: AtomicU64::new(0),
}
}
#[inline]
pub(crate) fn load<'a>(&'a self, _guard: &'a Guard) -> CellRef<'a, T> {
let ptr = self.current.load(Ordering::Acquire);
let value = unsafe { &*ptr };
CellRef { value }
}
#[inline]
pub(crate) unsafe fn get_unpinned(&self) -> &T {
let ptr = self.current.load(Ordering::Acquire);
unsafe { &*ptr }
}
#[cfg(test)]
pub(crate) fn load_full(&self) -> Arc<T> {
let guard = crossbeam_epoch::pin();
self.load(&guard).to_arc()
}
pub(crate) fn store(&self, value: Arc<T>) {
let new_ptr = Arc::into_raw(value).cast_mut();
let old_ptr = self.current.swap(new_ptr, Ordering::AcqRel);
let old = unsafe { Arc::from_raw(old_ptr) };
let id = self.next_retire_id.fetch_add(1, Ordering::Relaxed);
self.retired.lock().push((id, old));
let retired: Weak<RetiredList<T>> = Arc::downgrade(&self.retired);
let guard = crossbeam_epoch::pin();
guard.defer(move || {
if let Some(list) = retired.upgrade() {
list.lock().retain(|(entry, _)| *entry != id);
}
});
guard.flush();
}
pub(crate) fn set_mut(&mut self, value: Arc<T>) {
let new_ptr = Arc::into_raw(value).cast_mut();
let old_ptr = std::mem::replace(self.current.get_mut(), new_ptr);
drop(unsafe { Arc::from_raw(old_ptr) });
self.retired.lock().clear();
}
}
impl<T: Send + Sync + 'static> Drop for ArcCell<T> {
fn drop(&mut self) {
let ptr = *self.current.get_mut();
drop(unsafe { Arc::from_raw(ptr) });
self.retired.lock().clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn mapping(bytes: &[u8]) -> (std::path::PathBuf, Arc<Mmap>) {
let mut path = std::env::temp_dir();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
path.push(format!(
"emdb-arc-cell-{}-{nanos}-{}",
std::process::id(),
bytes.len()
));
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(bytes).unwrap();
drop(file);
let file = std::fs::File::open(&path).unwrap();
let mmap = unsafe { Mmap::map(&file).unwrap() };
(path, Arc::new(mmap))
}
#[test]
fn test_arc_cell_store_publishes_new_mapping() {
let (p1, m1) = mapping(b"one");
let (p2, m2) = mapping(b"two!");
let cell = ArcCell::new(m1);
let guard = crossbeam_epoch::pin();
let before = cell.load(&guard);
cell.store(m2);
assert_eq!(before.bytes(), b"one");
assert_eq!(cell.load(&guard).bytes(), b"two!");
drop(guard);
assert_eq!(&cell.load_full()[..], b"two!");
drop(cell);
let _ = std::fs::remove_file(p1);
let _ = std::fs::remove_file(p2);
}
#[test]
fn test_arc_cell_drop_releases_every_value() {
let (p1, m1) = mapping(b"a");
let (p2, m2) = mapping(b"bb");
let weak1 = Arc::downgrade(&m1);
let weak2 = Arc::downgrade(&m2);
let cell = ArcCell::new(m1);
cell.store(m2);
drop(cell);
assert!(weak1.upgrade().is_none(), "retired value outlived the cell");
assert!(weak2.upgrade().is_none(), "current value outlived the cell");
let _ = std::fs::remove_file(p1);
let _ = std::fs::remove_file(p2);
}
#[test]
fn test_arc_cell_set_mut_releases_old_and_retired_values_now() {
let a = Arc::new(1_u32);
let b = Arc::new(2_u32);
let weak_a = Arc::downgrade(&a);
let weak_b = Arc::downgrade(&b);
let mut cell = ArcCell::new(a);
cell.store(b);
cell.set_mut(Arc::new(3));
assert!(weak_a.upgrade().is_none(), "retired value kept");
assert!(weak_b.upgrade().is_none(), "replaced value kept");
assert_eq!(*cell.load_full(), 3);
}
#[test]
fn test_arc_cell_to_arc_outlives_guard_and_cell() {
let (p1, m1) = mapping(b"keep");
let cell = ArcCell::new(m1);
let held = {
let guard = crossbeam_epoch::pin();
cell.load(&guard).to_arc()
};
drop(cell);
assert_eq!(&held[..], b"keep");
drop(held);
let _ = std::fs::remove_file(p1);
}
#[test]
fn test_arc_cell_retired_values_are_reclaimed() {
let cell = ArcCell::new(Arc::new(0_u32));
for i in 0..200_u32 {
cell.store(Arc::new(i));
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while cell.retired.lock().len() >= 200 && std::time::Instant::now() < deadline {
crossbeam_epoch::pin().flush();
}
assert!(cell.retired.lock().len() < 200);
}
}