#![cfg(all(loom, feature = "persistence"))]
use loom::sync::Arc;
use loom::thread;
#[derive(Clone, Debug, PartialEq)]
struct TestEdge {
id: u64,
source: u64,
target: u64,
label: String,
}
impl TestEdge {
fn new(id: u64, source: u64, target: u64, label: &str) -> Self {
Self {
id,
source,
target,
label: label.to_string(),
}
}
}
mod loom_edge_store {
use super::TestEdge;
use loom::sync::{Arc, RwLock};
use std::collections::HashMap;
pub struct LoomEdgeStore {
shards: Vec<RwLock<HashMap<u64, TestEdge>>>,
edge_ids: RwLock<HashMap<u64, u64>>, num_shards: usize,
}
impl LoomEdgeStore {
pub fn new(num_shards: usize) -> Self {
let shards = (0..num_shards)
.map(|_| RwLock::new(HashMap::new()))
.collect();
Self {
shards,
edge_ids: RwLock::new(HashMap::new()),
num_shards,
}
}
fn shard_index(&self, node_id: u64) -> usize {
(node_id as usize) % self.num_shards
}
pub fn add_edge(&self, edge: TestEdge) -> Result<(), &'static str> {
let edge_id = edge.id;
let source_id = edge.source;
let mut ids = self.edge_ids.write().unwrap();
if ids.contains_key(&edge_id) {
return Err("edge exists");
}
let source_shard = self.shard_index(edge.source);
let target_shard = self.shard_index(edge.target);
if source_shard == target_shard {
let mut guard = self.shards[source_shard].write().unwrap();
guard.insert(edge_id, edge);
} else {
let (first_idx, second_idx) = if source_shard < target_shard {
(source_shard, target_shard)
} else {
(target_shard, source_shard)
};
let mut first = self.shards[first_idx].write().unwrap();
let mut second = self.shards[second_idx].write().unwrap();
if source_shard < target_shard {
first.insert(edge_id, edge.clone());
} else {
second.insert(edge_id, edge.clone());
}
}
ids.insert(edge_id, source_id);
Ok(())
}
pub fn contains_edge(&self, edge_id: u64) -> bool {
self.edge_ids.read().unwrap().contains_key(&edge_id)
}
pub fn get_outgoing(&self, node_id: u64) -> Vec<TestEdge> {
let shard = &self.shards[self.shard_index(node_id)];
let guard = shard.read().unwrap();
guard
.values()
.filter(|e| e.source == node_id)
.cloned()
.collect()
}
pub fn edge_count(&self) -> usize {
self.edge_ids.read().unwrap().len()
}
}
}
use loom_edge_store::LoomEdgeStore;
#[test]
fn test_loom_concurrent_edge_insert_cross_shard() {
loom::model(|| {
let store = Arc::new(LoomEdgeStore::new(4));
let s1 = Arc::clone(&store);
let t1 = thread::spawn(move || {
let _ = s1.add_edge(TestEdge::new(1, 0, 1, "edge_a"));
});
let s2 = Arc::clone(&store);
let t2 = thread::spawn(move || {
let _ = s2.add_edge(TestEdge::new(2, 1, 0, "edge_b"));
});
t1.join().unwrap();
t2.join().unwrap();
assert!(store.contains_edge(1));
assert!(store.contains_edge(2));
assert_eq!(store.edge_count(), 2);
});
}
#[test]
fn test_loom_concurrent_read_write() {
loom::model(|| {
let store = Arc::new(LoomEdgeStore::new(4));
store.add_edge(TestEdge::new(1, 0, 1, "initial")).unwrap();
let s1 = Arc::clone(&store);
let t1 = thread::spawn(move || {
let _ = s1.add_edge(TestEdge::new(2, 0, 2, "new_edge"));
});
let s2 = Arc::clone(&store);
let t2 = thread::spawn(move || {
let edges = s2.get_outgoing(0);
assert!(edges
.iter()
.any(|e| e.id == 1 && e.source == 0 && e.target == 1));
});
t1.join().unwrap();
t2.join().unwrap();
assert!(store.contains_edge(1));
assert!(store.contains_edge(2));
});
}
#[test]
fn test_loom_duplicate_edge_prevention() {
loom::model(|| {
let store = Arc::new(LoomEdgeStore::new(4));
let s1 = Arc::clone(&store);
let t1 = thread::spawn(move || s1.add_edge(TestEdge::new(1, 0, 1, "edge")));
let s2 = Arc::clone(&store);
let t2 = thread::spawn(move || s2.add_edge(TestEdge::new(1, 0, 1, "edge")));
let r1 = t1.join().unwrap();
let r2 = t2.join().unwrap();
assert!((r1.is_ok() && r2.is_err()) || (r1.is_err() && r2.is_ok()));
assert_eq!(store.edge_count(), 1);
});
}
#[test]
fn test_loom_parallel_insert_no_contention() {
loom::model(|| {
let store = Arc::new(LoomEdgeStore::new(4));
let s1 = Arc::clone(&store);
let t1 = thread::spawn(move || {
let _ = s1.add_edge(TestEdge::new(1, 0, 0, "self_loop_0"));
});
let s2 = Arc::clone(&store);
let t2 = thread::spawn(move || {
let _ = s2.add_edge(TestEdge::new(2, 1, 1, "self_loop_1"));
});
let s3 = Arc::clone(&store);
let t3 = thread::spawn(move || {
let _ = s3.add_edge(TestEdge::new(3, 2, 2, "self_loop_2"));
});
t1.join().unwrap();
t2.join().unwrap();
t3.join().unwrap();
assert_eq!(store.edge_count(), 3);
assert!(store.contains_edge(1) && store.contains_edge(2) && store.contains_edge(3));
assert_eq!(store.get_outgoing(0).len(), 1);
assert_eq!(store.get_outgoing(1).len(), 1);
assert_eq!(store.get_outgoing(2).len(), 1);
});
}
mod loom_payload_guard {
use loom::sync::{Mutex, RwLock};
use std::collections::HashSet;
pub struct FixedGraph {
payload: RwLock<HashSet<u64>>,
wal: Mutex<()>,
edges: RwLock<HashSet<(u64, u64)>>,
}
impl FixedGraph {
pub fn new() -> Self {
Self {
payload: RwLock::new(HashSet::new()),
wal: Mutex::new(()),
edges: RwLock::new(HashSet::new()),
}
}
pub fn seed(&self, node: u64) {
self.payload.write().unwrap().insert(node);
}
pub fn add_edge(&self, source: u64, target: u64) -> bool {
let payload = self.payload.read().unwrap();
if !payload.contains(&source) || !payload.contains(&target) {
return false;
}
let _wal = self.wal.lock().unwrap();
self.edges.write().unwrap().insert((source, target));
true
}
pub fn delete(&self, node: u64) {
self.payload.write().unwrap().remove(&node);
let _wal = self.wal.lock().unwrap();
self.edges
.write()
.unwrap()
.retain(|(s, t)| *s != node && *t != node);
}
pub fn has_phantom_edge(&self, node: u64) -> bool {
let payload_gone = !self.payload.read().unwrap().contains(&node);
let edge_remains = self
.edges
.read()
.unwrap()
.iter()
.any(|(s, t)| *s == node || *t == node);
payload_gone && edge_remains
}
}
}
use loom_payload_guard::FixedGraph;
#[test]
fn loom_payload_guard_excludes_delete_during_edge_write() {
loom::model(|| {
let graph = Arc::new(FixedGraph::new());
graph.seed(1);
graph.seed(2);
let g1 = Arc::clone(&graph);
let t1 = thread::spawn(move || {
g1.add_edge(1, 2);
});
let g2 = Arc::clone(&graph);
let t2 = thread::spawn(move || {
g2.delete(1);
});
t1.join().unwrap();
t2.join().unwrap();
assert!(
!graph.has_phantom_edge(1),
"fixed lock order must never leave a phantom edge"
);
});
}