#![allow(deprecated)]
#![cfg(feature = "experimental")]
use std::sync::Arc;
use std::thread::scope;
use proptest::prelude::*;
use sefer_alloc::{ShardedHandle, ShardedRegion};
struct Payload {
id: u64,
drops: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl PartialEq for Payload {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Drop for Payload {
fn drop(&mut self) {
self.drops
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
#[derive(Clone, Debug)]
enum Op {
InsertInto(usize, u64),
Remove(usize),
Get(usize),
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn sharded_region_matches_per_shard_reference_model(
n_shards in 1usize..=4,
ops in prop::collection::vec(
prop_oneof![
(any::<usize>(), any::<u64>()).prop_map(|(s, v)| Op::InsertInto(s, v)),
any::<usize>().prop_map(Op::Remove),
any::<usize>().prop_map(Op::Get),
],
0..200,
)
) {
const CAP_PER_SHARD: usize = 8;
let region = ShardedRegion::<Payload>::with_shards(n_shards, CAP_PER_SHARD);
let drops = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0usize));
let mut live: Vec<(ShardedHandle<Payload>, u64, u16)> = Vec::new();
let mut total_inserts = 0usize;
let mut total_constructed = 0usize;
prop_assert_eq!(region.shard_count(), n_shards);
prop_assert!(region.is_empty());
let mut expected_next_shard: u16 = 0;
for op in ops {
match op {
Op::InsertInto(_shard_hint, v) => {
ShardedRegion::<Payload>::_reset_my_shard_binding_for_tests();
let p = Payload { id: v, drops: Arc::clone(&drops) };
total_constructed += 1;
match region.insert(p) {
Ok(h) => {
prop_assert_eq!(
h.shard(),
expected_next_shard,
"round-robin must claim shards in order after a TLS reset"
);
prop_assert_eq!(
region.get_with(h, |p| p.id),
Some(v),
"fresh handle must resolve via router (I1)"
);
live.push((h, v, expected_next_shard));
total_inserts += 1;
expected_next_shard =
u16::try_from((usize::from(expected_next_shard) + 1) % n_shards)
.unwrap();
}
Err(_returned) => {
expected_next_shard =
u16::try_from((usize::from(expected_next_shard) + 1) % n_shards)
.unwrap();
}
}
}
Op::Remove(n) => {
if !live.is_empty() {
let i = n % live.len();
let (h, _v, _shard) = live.swap_remove(i);
let removed = region.remove(h);
prop_assert!(removed, "live handle must remove once");
prop_assert_eq!(region.get_with(h, |p| p.id), None);
prop_assert!(
!region.remove(h),
"second remove of a stale handle is a no-op false (I2)"
);
}
}
Op::Get(n) => {
if !live.is_empty() {
let i = n % live.len();
let (h, v, _shard) = live[i];
prop_assert_eq!(
region.get_with(h, |p| p.id),
Some(v),
"live handle must resolve to its value (I1)"
);
}
}
}
prop_assert_eq!(region.len(), live.len());
prop_assert_eq!(region.is_empty(), live.is_empty());
}
for (h, v, _shard) in &live {
prop_assert_eq!(region.get_with(*h, |p| p.id), Some(*v));
}
let survivors = live.len();
drop(region);
drop(live);
let observed_drops = drops.load(std::sync::atomic::Ordering::Relaxed);
prop_assert!(
observed_drops >= survivors,
"at least every survivor must be dropped once by region-drop \
(survivors={survivors}, observed_drops={observed_drops})"
);
prop_assert!(
observed_drops <= total_constructed,
"no double-free: drops ({observed_drops}) must not exceed constructed payloads ({total_constructed})"
);
prop_assert!(total_inserts <= total_constructed);
}
}
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 CONC_THREADS: usize = 4;
const CONC_OPS_PER_THREAD: usize = 2_000;
#[test]
fn concurrent_routed_writers_never_corrupt_and_accounting_holds() {
const N_SHARDS: usize = 2;
const CAP_PER_SHARD: usize = CONC_OPS_PER_THREAD * CONC_THREADS;
let region = Arc::new(ShardedRegion::<Tagged>::with_shards(
N_SHARDS,
CAP_PER_SHARD,
));
let total_survivors = scope(|scope| {
let handles: Vec<_> = (0..CONC_THREADS)
.map(|tid| {
let region = Arc::clone(®ion);
scope.spawn(move || worker(tid, ®ion))
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("worker thread panicked"))
.sum::<usize>()
});
assert_eq!(
region.len(),
total_survivors,
"len() must equal the total live entries threads reported (I4 across shards)"
);
assert_eq!(region.is_empty(), total_survivors == 0);
assert_eq!(region.shard_count(), N_SHARDS);
}
fn worker(tid: usize, region: &ShardedRegion<Tagged>) -> usize {
let mut rng = Lcg::new(
u64::try_from(tid)
.unwrap()
.wrapping_add(0x9E37_79B9_7F4A_7C15),
);
let mut my_handles: Vec<ShardedHandle<Tagged>> = Vec::with_capacity(CONC_OPS_PER_THREAD);
for seq in 0..u64::try_from(CONC_OPS_PER_THREAD).unwrap() {
let value = Tagged { thread: tid, seq };
let h = region
.insert(value.clone())
.expect("per-shard capacity sized to total ops; insert must succeed");
let got = region
.get_cloned(h)
.expect("fresh handle must resolve via router");
assert_eq!(
got, value,
"thread {tid}: immediate re-read returned a different value"
);
my_handles.push(h);
if !my_handles.is_empty() && rng.chance(1, 4) {
let idx = rng.below(my_handles.len());
let victim = my_handles.swap_remove(idx);
let removed = region.remove(victim);
assert!(
removed,
"thread {tid}: our own live handle must remove exactly once"
);
assert_eq!(
region.get_cloned(victim),
None,
"thread {tid}: removed handle must resolve to None (I2)"
);
}
}
for h in &my_handles {
let v = region
.get_cloned(*h)
.expect("a tracked handle must still resolve via router");
assert_eq!(
v.thread, tid,
"thread {tid}: surviving handle resolved to a value from a different thread"
);
}
my_handles.len()
}
#[test]
fn router_uses_handle_shard_and_handles_from_distinct_shards_are_distinct() {
let region = ShardedRegion::<u64>::with_shards(3, 8);
let h0 = region.insert(111).expect("insert into shard 0");
assert_eq!(
h0.shard(),
0,
"first insert from this thread claims shard 0"
);
ShardedRegion::<u64>::_reset_my_shard_binding_for_tests();
let h1 = region.insert(222).expect("insert into shard 1");
assert_eq!(h1.shard(), 1, "after reset, next insert claims shard 1");
assert_eq!(region.get_cloned(h0), Some(111));
assert_eq!(region.get_cloned(h1), Some(222));
assert_ne!(
h0, h1,
"handles from different shards are distinct by their shard field"
);
let (_, inner0) = ShardedRegion::<u64>::split_handle(h0);
let (_, inner1) = ShardedRegion::<u64>::split_handle(h1);
assert_eq!(
inner0, inner1,
"both shards claim the lowest index first, so inner handles coincide; \
the shard field is what distinguishes them"
);
assert!(region.remove(h0));
assert_eq!(region.get_cloned(h0), None, "h0 removed");
assert_eq!(
region.get_cloned(h1),
Some(222),
"h1 in a different shard is unaffected"
);
assert_eq!(region.len(), 1);
}