#![cfg(loom)]
use loom::sync::atomic::{AtomicU32, Ordering};
use loom::sync::{Arc, Mutex, RwLock};
use std::collections::HashMap;
#[derive(Default)]
struct MockKernel {
refs: Mutex<HashMap<u32, (u32, u32)>>,
bc_acquire_count: AtomicU32,
bc_release_count: AtomicU32,
bc_increfs_count: AtomicU32,
bc_decrefs_count: AtomicU32,
saw_acquire_to_freed_slot: AtomicU32,
}
#[derive(Debug, PartialEq, Eq)]
struct DeadObject;
impl MockKernel {
fn new() -> Self {
Self::default()
}
fn bc_acquire(&self, h: u32) -> Result<(), DeadObject> {
self.bc_acquire_count.fetch_add(1, Ordering::Relaxed);
let mut refs = self.refs.lock().unwrap();
let entry = refs.entry(h).or_insert((0, 0));
if entry.0 == 0 && entry.1 == 0 {
self.saw_acquire_to_freed_slot
.fetch_add(1, Ordering::Relaxed);
return Err(DeadObject);
}
entry.0 += 1;
Ok(())
}
fn bc_release(&self, h: u32) -> Result<(), DeadObject> {
self.bc_release_count.fetch_add(1, Ordering::Relaxed);
let mut refs = self.refs.lock().unwrap();
let entry = refs.get_mut(&h).ok_or(DeadObject)?;
if entry.0 == 0 {
return Err(DeadObject);
}
entry.0 -= 1;
Ok(())
}
fn bc_increfs(&self, h: u32) -> Result<(), DeadObject> {
self.bc_increfs_count.fetch_add(1, Ordering::Relaxed);
let mut refs = self.refs.lock().unwrap();
let entry = refs.entry(h).or_insert((0, 0));
entry.1 += 1;
Ok(())
}
#[allow(dead_code)]
fn bc_decrefs(&self, h: u32) -> Result<(), DeadObject> {
self.bc_decrefs_count.fetch_add(1, Ordering::Relaxed);
let mut refs = self.refs.lock().unwrap();
let entry = refs.get_mut(&h).ok_or(DeadObject)?;
if entry.1 == 0 {
return Err(DeadObject);
}
entry.1 -= 1;
Ok(())
}
fn ref_state(&self, h: u32) -> (u32, u32) {
let refs = self.refs.lock().unwrap();
refs.get(&h).copied().unwrap_or((0, 0))
}
}
struct MockProxyHandle {
handle: u32,
kernel: Arc<MockKernel>,
}
impl Drop for MockProxyHandle {
fn drop(&mut self) {
let _ = self.kernel.bc_release(self.handle);
}
}
type Cache = RwLock<HashMap<u32, ()>>;
fn strong_proxy_for_handle(
cache: &Cache,
kernel: &Arc<MockKernel>,
handle: u32,
) -> Result<Arc<MockProxyHandle>, DeadObject> {
let pin_already_held = {
let read = cache.read().unwrap();
read.contains_key(&handle)
};
if !pin_already_held {
let mut write = cache.write().unwrap();
if let std::collections::hash_map::Entry::Vacant(slot) = write.entry(handle) {
kernel.bc_increfs(handle)?;
slot.insert(());
}
}
kernel.bc_acquire(handle)?;
Ok(Arc::new(MockProxyHandle {
handle,
kernel: Arc::clone(kernel),
}))
}
#[test]
fn cache_pin_holds_under_concurrent_lookup_and_drop() {
const HANDLE: u32 = 42;
loom::model(|| {
let kernel = Arc::new(MockKernel::new());
let cache: Arc<Cache> = Arc::new(RwLock::new(HashMap::new()));
let kernel_t1 = Arc::clone(&kernel);
let cache_t1 = Arc::clone(&cache);
let t1 = loom::thread::spawn(move || {
let arc = strong_proxy_for_handle(&cache_t1, &kernel_t1, HANDLE)
.expect("T1 lookup must succeed");
drop(arc);
});
let kernel_t2 = Arc::clone(&kernel);
let cache_t2 = Arc::clone(&cache);
let t2 = loom::thread::spawn(move || {
let arc = strong_proxy_for_handle(&cache_t2, &kernel_t2, HANDLE)
.expect("T2 lookup must succeed");
drop(arc);
});
t1.join().unwrap();
t2.join().unwrap();
assert_eq!(
kernel.saw_acquire_to_freed_slot.load(Ordering::Relaxed),
0,
"I1 violation: BC_ACQUIRE issued against freed kernel slot"
);
let (strong, weak) = kernel.ref_state(HANDLE);
assert_eq!(
strong, 0,
"after both threads' Arcs dropped, kernel strong must be 0; got {strong}"
);
assert_eq!(
weak, 1,
"cache pin must keep kernel weak == 1 (I1); got {weak}"
);
let increfs = kernel.bc_increfs_count.load(Ordering::Relaxed);
let decrefs = kernel.bc_decrefs_count.load(Ordering::Relaxed);
let acquire = kernel.bc_acquire_count.load(Ordering::Relaxed);
let release = kernel.bc_release_count.load(Ordering::Relaxed);
assert_eq!(
increfs, 1,
"cache pin must be issued exactly once per handle; got increfs={increfs}"
);
assert_eq!(
decrefs, 0,
"no obituary in this model; got decrefs={decrefs}"
);
assert_eq!(
acquire, 2,
"two lookups → two BC_ACQUIREs; got acquire={acquire}"
);
assert_eq!(
release, 2,
"two Arc Drops → two BC_RELEASEs; got release={release}"
);
});
}
#[test]
fn case_b_path_reuses_existing_pin() {
const HANDLE: u32 = 7;
loom::model(|| {
let kernel = Arc::new(MockKernel::new());
let cache: Arc<Cache> = Arc::new(RwLock::new(HashMap::new()));
let arc1 =
strong_proxy_for_handle(&cache, &kernel, HANDLE).expect("first lookup must succeed");
drop(arc1);
let (s_mid, w_mid) = kernel.ref_state(HANDLE);
assert_eq!(s_mid, 0, "post-drop strong must be 0");
assert_eq!(w_mid, 1, "pin keeps weak == 1 across lookup-then-drop");
let kernel_t2 = Arc::clone(&kernel);
let cache_t2 = Arc::clone(&cache);
let t2 = loom::thread::spawn(move || {
let arc = strong_proxy_for_handle(&cache_t2, &kernel_t2, HANDLE)
.expect("case-b lookup must succeed under pin");
drop(arc);
});
t2.join().unwrap();
assert_eq!(
kernel.saw_acquire_to_freed_slot.load(Ordering::Relaxed),
0,
"I1 violation: case-b BC_ACQUIRE saw freed slot"
);
assert_eq!(
kernel.bc_increfs_count.load(Ordering::Relaxed),
1,
"case (b) must NOT issue a second BC_INCREFS"
);
let (strong, weak) = kernel.ref_state(HANDLE);
assert_eq!(strong, 0);
assert_eq!(weak, 1, "pin survives case-b resurrection");
});
}