#![allow(deprecated)]
#![cfg(feature = "experimental")]
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::scope;
use sefer_alloc::LockFreeRegion;
#[test]
fn single_threaded_sequence_matches_reference_model() {
let region = LockFreeRegion::<u32>::new();
let mut model: Vec<(sefer_alloc::LockFreeHandle<u32>, u32)> = Vec::new();
assert!(region.is_empty());
assert_eq!(region.len(), 0);
for v in [10_u32, 20, 30, 40, 50] {
let h = region.insert(v);
assert_eq!(*region.get(h).expect("fresh handle must resolve"), v);
model.push((h, v));
}
assert_eq!(region.len(), model.len());
assert!(!region.is_empty());
for (h, v) in &model {
assert_eq!(*region.get(*h).expect("live handle must resolve"), *v);
assert!(region.contains(*h));
}
let victim_idx = 2;
let (victim_h, victim_v) = model.remove(victim_idx);
let removed = region
.remove(victim_h)
.expect("live handle must remove once");
assert_eq!(*removed, victim_v);
assert_eq!(
region.get(victim_h),
None,
"removed handle must be None (I2)"
);
assert!(!region.contains(victim_h));
assert_eq!(
region.remove(victim_h),
None,
"second remove of a stale handle is a no-op None (I2)"
);
assert_eq!(region.len(), model.len());
for (h, v) in &model {
assert_eq!(*region.get(*h).expect("survivor must still resolve"), *v);
}
let new_v = 999_u32;
let new_h = region.insert(new_v);
assert_eq!(*region.get(new_h).expect("new handle must resolve"), new_v);
assert_eq!(
region.get(victim_h),
None,
"stale handle to a reused slot must not resolve (I3)"
);
assert_ne!(new_h, victim_h, "fresh handle differs from the stale one");
model.push((new_h, new_v));
assert_eq!(region.len(), model.len());
while let Some((h, _v)) = model.pop() {
assert!(region.remove(h).is_some(), "live handle must remove");
assert_eq!(region.get(h), None);
}
assert_eq!(region.len(), 0);
assert!(region.is_empty());
}
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed.max(1))
}
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0
}
fn chance(&mut self, num: u32, denom: u32) -> bool {
if denom == 0 {
return false;
}
(self.next_u64() % u64::from(denom)) < u64::from(num)
}
fn below(&mut self, n: usize) -> usize {
let n64 = u64::try_from(n).expect("index space fits u64");
usize::try_from(self.next_u64() % n64).expect("modulo result fits usize")
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Tagged {
thread: usize,
seq: u64,
}
const WRITERS: usize = 3;
const READERS: usize = 3;
const WRITER_OPS: usize = 1_500;
const READER_OPS: usize = 4_000;
type HandlePool<T> = Arc<Mutex<Vec<(usize, sefer_alloc::LockFreeHandle<T>)>>>;
#[test]
fn concurrent_readers_and_writers_never_observe_a_torn_value() {
let region = Arc::new(LockFreeRegion::<Tagged>::new());
let pool: HandlePool<Tagged> = Arc::new(Mutex::new(Vec::new()));
let total_survivors = scope(|scope| {
let live = Arc::new(AtomicBool::new(true));
let mut writer_handles: Vec<_> = (0..WRITERS)
.map(|tid| {
let region = Arc::clone(®ion);
let pool = Arc::clone(&pool);
scope.spawn(move || writer(tid, ®ion, &pool))
})
.collect();
let mut reader_handles: Vec<_> = Vec::with_capacity(READERS);
for rid in 0..READERS {
let region = Arc::clone(®ion);
let pool = Arc::clone(&pool);
let live = Arc::clone(&live);
reader_handles.push(scope.spawn(move || reader(rid, ®ion, &pool, &live)));
}
let survivors: usize = writer_handles
.drain(..)
.map(|h| h.join().expect("writer thread panicked"))
.sum();
live.store(false, Ordering::Relaxed);
for h in reader_handles {
h.join().expect("reader thread panicked");
}
survivors
});
assert_eq!(
region.len(),
total_survivors,
"len() must equal the total live entries writers reported (I4 under concurrency)"
);
assert_eq!(region.is_empty(), total_survivors == 0);
}
fn writer(
tid: usize,
region: &LockFreeRegion<Tagged>,
pool: &Mutex<Vec<(usize, sefer_alloc::LockFreeHandle<Tagged>)>>,
) -> usize {
let mut rng = Lcg::new(
u64::try_from(tid)
.unwrap()
.wrapping_add(0x9E37_79B9_7F4A_7C15),
);
let mut my_handles: Vec<sefer_alloc::LockFreeHandle<Tagged>> = Vec::with_capacity(WRITER_OPS);
for seq in 0..u64::try_from(WRITER_OPS).unwrap() {
let value = Tagged { thread: tid, seq };
let h = region.insert(value.clone());
let got = region.get(h).expect("fresh handle must resolve");
assert_eq!(
*got, value,
"writer {tid}: immediate re-read returned a different value"
);
my_handles.push(h);
pool.lock().expect("pool mutex poisoned").push((tid, h));
if !my_handles.is_empty() && rng.chance(1, 3) {
let idx = rng.below(my_handles.len());
let victim = my_handles.swap_remove(idx);
let removed = region
.remove(victim)
.expect("our own live handle must remove exactly once");
assert_eq!(
removed.thread, tid,
"writer {tid}: removed a value from a different thread"
);
assert_eq!(
region.get(victim),
None,
"writer {tid}: removed handle must resolve to None (I2)"
);
}
}
my_handles.len()
}
static PROBES: AtomicUsize = AtomicUsize::new(0);
fn reader(
rid: usize,
region: &LockFreeRegion<Tagged>,
pool: &Mutex<Vec<(usize, sefer_alloc::LockFreeHandle<Tagged>)>>,
live: &AtomicBool,
) {
let mut rng = Lcg::new(
u64::try_from(rid)
.unwrap()
.wrapping_add(0x51ED_270B_1F2C_3D4E)
.wrapping_mul(0x9E37_79B9_7F4A_7C15),
);
loop {
let done =
!live.load(Ordering::Relaxed) && PROBES.load(Ordering::Relaxed) >= READER_OPS * READERS;
if done {
break;
}
let probe = {
let guard = pool.lock().expect("pool mutex poisoned");
if guard.is_empty() {
None
} else {
let (owner, h) = guard[rng.below(guard.len())];
Some((owner, h))
}
};
if let Some((owner, h)) = probe {
PROBES.fetch_add(1, Ordering::Relaxed);
if let Some(v) = region.get(h) {
assert_eq!(
v.thread, owner,
"reader {rid}: resolved value belongs to writer {} but the handle was \
owned by writer {} — torn/cross read detected",
v.thread, owner,
);
assert!(
v.seq < u64::try_from(WRITER_OPS).unwrap(),
"reader {rid}: resolved value seq {} is out of range — torn read detected",
v.seq
);
}
}
}
}