1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Concurrent stress test over `Arc<SyncRegion<T>>` (Phase 3a).
//!
//! The core property asserted here, beyond "didn't panic":
//!
//! > **No cross-thread corruption:** under random interleavings of insert /
//! > read / remove across several threads, a handle *always* resolves to the
//! > value its own thread inserted, or to `None` — never to a *different*
//! > value. The `RwLock` serialises every mutation against every read.
//!
//! Plus, after all threads join, `len()` equals the total survivors the threads
//! collectively reported (I4 — accounting holds under concurrency).
//!
//! Uses a per-thread fixed-seed LCG (no `rand` crate). Bounded to stay fast per
//! the short-scenario policy.
use std::sync::Arc;
use std::thread::scope;
use sefer_alloc::SyncRegion;
/// Per-thread fixed-seed LCG (Numerical Recipes constants). Deterministic per
/// thread index, so the test is reproducible across runs.
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed.max(1))
}
/// Next pseudo-random `u64`.
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
}
/// `true` with probability `num / denom`.
fn chance(&mut self, num: u32, denom: u32) -> bool {
if denom == 0 {
return false;
}
(self.next_u64() % u64::from(denom)) < u64::from(num)
}
/// A pseudo-random index in `0..n` (`n` must be > 0). Cast-free so the test
/// stays clippy-pedantic clean.
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")
}
}
/// A value tagged with the id of the thread that inserted it, so a read can
/// prove it belongs to the right handle and was not crossed with another.
#[derive(Clone, Debug, PartialEq, Eq)]
struct Tagged {
thread: usize,
seq: u64,
}
const THREADS: usize = 4;
const OPS_PER_THREAD: usize = 2_000;
#[test]
fn concurrent_insert_read_remove_never_corrupts_and_accounting_holds() {
let region = Arc::new(SyncRegion::<Tagged>::with_capacity(
OPS_PER_THREAD * THREADS,
));
let total_survivors = scope(|scope| {
let handles: Vec<_> = (0..THREADS)
.map(|tid| {
let region = Arc::clone(®ion);
// Each thread returns how many of its own handles it leaves alive.
scope.spawn(move || worker(tid, ®ion))
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("worker thread panicked"))
.sum::<usize>()
});
// The core accounting property (I4) under concurrency.
assert_eq!(
region.len(),
total_survivors,
"len() must equal the total live entries threads reported"
);
// And is_empty only when truly empty — sanity, the region has survivors here
// unless all threads happened to remove everything they inserted.
assert_eq!(region.is_empty(), total_survivors == 0);
}
fn worker(tid: usize, region: &SyncRegion<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::Handle<Tagged>> = Vec::with_capacity(OPS_PER_THREAD);
for seq in 0..u64::try_from(OPS_PER_THREAD).unwrap() {
// Insert a value uniquely identifiable by (thread, seq).
let value = Tagged { thread: tid, seq };
let h = region.insert(value.clone());
// Read it back IMMEDIATELY and assert it resolves to its own value, not
// a different one — the per-handle property under contention.
let got = region.get_cloned(h).expect("fresh handle must resolve");
assert_eq!(
got, value,
"immediate re-read of a fresh handle returned a different value"
);
my_handles.push(h);
// Randomly remove one of our own handles this iteration, chosen at
// random and POPPED from our tracking vec so every probe hits a still
// -live handle and the removes actually churn. We only ever touch
// handles our own thread created, so this is ownership-safe.
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)
.expect("our own live handle must remove exactly once");
assert_eq!(
removed.thread, tid,
"removed a value from a different thread"
);
// After remove, the handle must be None forever (I2).
assert_eq!(
region.get_cloned(victim),
None,
"removed handle must resolve to None"
);
}
}
// After the churn, `my_handles` holds exactly our still-live handles (we
// popped every removed one). Re-check each resolves to *our* value under a
// single read guard — a final cross-thread re-assert of the no-corruption
// property — and report the survivor count for the accounting check.
let guard = region.read();
for h in &my_handles {
let v = guard
.get(*h)
.expect("a tracked (un-removed) handle must still resolve");
assert_eq!(
v.thread, tid,
"surviving handle resolved to a value from a different thread"
);
}
my_handles.len()
}