use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard};
const PRUNE_THRESHOLD: usize = 1024;
#[derive(Clone, Default)]
pub(crate) struct SubjectLocks {
entries: Arc<Mutex<HashMap<String, Arc<AsyncMutex<()>>>>>,
}
impl SubjectLocks {
pub(crate) async fn lock(&self, subject_key: &str) -> OwnedMutexGuard<()> {
let entry = {
let mut map = self
.entries
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if map.len() > PRUNE_THRESHOLD {
map.retain(|_, lock| Arc::strong_count(lock) > 1);
}
map.entry(subject_key.to_string())
.or_insert_with(|| Arc::new(AsyncMutex::new(())))
.clone()
};
entry.lock_owned().await
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn blocks(locks: &SubjectLocks, key: &str) -> bool {
tokio::time::timeout(std::time::Duration::from_millis(50), locks.lock(key))
.await
.is_err()
}
#[tokio::test]
async fn the_same_subject_is_exclusive() {
let locks = SubjectLocks::default();
let held = locks.lock("did:ad:one").await;
assert!(
blocks(&locks, "did:ad:one").await,
"the same subject must serialise"
);
drop(held);
assert!(
!blocks(&locks, "did:ad:one").await,
"releasing must let the next writer in"
);
}
#[tokio::test]
async fn different_subjects_do_not_block_each_other() {
let locks = SubjectLocks::default();
let _one = locks.lock("did:ad:alpha").await;
assert!(
!blocks(&locks, "did:ad:beta").await,
"writes to unrelated resources must still run in parallel"
);
}
#[tokio::test]
async fn separate_stores_never_wait_on_each_other() {
let one = SubjectLocks::default();
let two = SubjectLocks::default();
let _held = one.lock("https://atomicdata.dev/properties/name").await;
assert!(
!blocks(&two, "https://atomicdata.dev/properties/name").await,
"a lock in one store must not block the same subject in another"
);
}
#[tokio::test]
async fn unused_entries_are_pruned_but_held_ones_survive() {
let locks = SubjectLocks::default();
let held = locks.lock("did:ad:kept").await;
for n in 0..PRUNE_THRESHOLD + 16 {
drop(locks.lock(&format!("did:ad:transient-{n}")).await);
}
let map = locks.entries.lock().unwrap();
assert!(
map.len() <= PRUNE_THRESHOLD + 16,
"the registry should have been pruned, holds {}",
map.len()
);
assert!(
map.contains_key("did:ad:kept"),
"a lock still held must never be pruned"
);
drop(map);
drop(held);
}
}