use std::borrow::Borrow;
use std::collections::HashSet;
use std::fmt;
use std::hash::Hash;
use std::sync::Arc;
use parking_lot::Mutex;
pub struct ClaimRegistry<K> {
keys: Arc<Mutex<HashSet<K>>>,
}
impl<K> Clone for ClaimRegistry<K> {
fn clone(&self) -> Self {
Self {
keys: Arc::clone(&self.keys),
}
}
}
impl<K: Eq + Hash + Clone> Default for ClaimRegistry<K> {
fn default() -> Self {
Self::new()
}
}
impl<K: Eq + Hash + Clone> ClaimRegistry<K> {
pub fn new() -> Self {
Self {
keys: Arc::new(Mutex::new(HashSet::new())),
}
}
#[must_use = "the claim releases the key when dropped; bind it for the whole refresh"]
pub fn claim(&self, key: K) -> Option<Claim<K>> {
{
let mut keys = self.keys.lock();
if !keys.insert(key.clone()) {
return None;
}
}
Some(Claim {
registry: self.clone(),
key,
})
}
#[must_use]
pub fn is_claimed<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.keys.lock().contains(key)
}
#[must_use]
pub fn len(&self) -> usize {
self.keys.lock().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.keys.lock().is_empty()
}
}
impl<K: fmt::Debug + Clone> fmt::Debug for ClaimRegistry<K> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let claimed = self.keys.lock().clone();
f.debug_struct("ClaimRegistry")
.field("claimed", &claimed)
.finish()
}
}
#[must_use = "a dropped claim releases the key immediately; bind it for the whole refresh"]
pub struct Claim<K: Eq + Hash> {
registry: ClaimRegistry<K>,
key: K,
}
impl<K: Eq + Hash> Claim<K> {
pub fn key(&self) -> &K {
&self.key
}
}
impl<K: Eq + Hash> Drop for Claim<K> {
fn drop(&mut self) {
self.registry.keys.lock().remove(&self.key);
}
}
impl<K: Eq + Hash + fmt::Debug> fmt::Debug for Claim<K> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Claim").field("key", &self.key).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::OnceLock;
use std::sync::mpsc;
use std::time::Duration;
#[test]
fn second_claim_of_a_live_key_is_none() {
let registry = ClaimRegistry::new();
let claim = registry.claim("a".to_string()).expect("first claim wins");
assert!(registry.claim("a".to_string()).is_none());
assert_eq!(claim.key(), "a");
assert_eq!(registry.len(), 1);
}
#[test]
fn drop_releases_the_key_and_a_re_claim_succeeds() {
let registry = ClaimRegistry::new();
let claim = registry.claim("a".to_string()).unwrap();
drop(claim);
assert!(!registry.is_claimed("a"));
assert!(registry.claim("a".to_string()).is_some());
}
#[test]
fn distinct_keys_are_claimed_independently_and_drain_to_empty() {
let registry = ClaimRegistry::new();
let a = registry.claim("a".to_string()).unwrap();
let b = registry.claim("b".to_string()).unwrap();
assert_eq!(registry.len(), 2);
drop(a);
assert_eq!(registry.len(), 1);
assert!(!registry.is_claimed("a"));
assert!(registry.is_claimed("b"));
drop(b);
assert_eq!(registry.len(), 0);
assert!(registry.is_empty());
}
#[test]
fn is_claimed_accepts_a_borrowed_key() {
let registry: ClaimRegistry<String> = ClaimRegistry::new();
assert!(!registry.is_claimed("a"));
let claim = registry.claim("a".to_string()).unwrap();
let borrowed: &str = "a";
assert!(registry.is_claimed(borrowed));
drop(claim);
assert!(!registry.is_claimed(borrowed));
}
#[test]
fn a_clone_of_the_registry_shares_the_claims() {
let registry = ClaimRegistry::new();
let other = registry.clone();
let claim = registry.claim(1).unwrap();
assert!(other.is_claimed(&1));
assert!(other.claim(1).is_none());
drop(claim);
assert!(other.is_empty());
}
#[test]
fn a_claim_outlives_the_registry_handle_it_came_from() {
let shared = ClaimRegistry::new();
let claim = {
let handle = shared.clone();
handle.claim(1).unwrap()
};
assert!(shared.is_claimed(&1));
drop(claim);
assert!(shared.is_empty());
}
#[test]
fn a_claim_is_send_for_a_send_key() {
fn assert_send<T: Send>(_: &T) {}
let registry: ClaimRegistry<String> = ClaimRegistry::new();
let claim = registry.claim("a".to_string()).unwrap();
assert_send(®istry);
assert_send(&claim);
}
static REENTRANT_REGISTRY: OnceLock<ClaimRegistry<ReentrantKey>> = OnceLock::new();
#[derive(Clone, PartialEq, Eq, Hash)]
struct ReentrantKey(u32);
impl fmt::Debug for ReentrantKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let live = REENTRANT_REGISTRY
.get()
.expect("registry installed before formatting")
.len();
write!(f, "ReentrantKey({}, live={live})", self.0)
}
}
fn recv_or_panic<T>(rx: &mpsc::Receiver<T>) -> T {
match rx.recv_timeout(Duration::from_secs(10)) {
Ok(sent) => sent,
Err(mpsc::RecvTimeoutError::Timeout) => {
panic!("formatting the registry deadlocked on the registry mutex")
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
panic!(
"the spawned thread panicked before sending, for a reason unrelated to the \
registry mutex (see the panic message printed above)"
)
}
}
}
#[test]
fn formatting_the_registry_does_not_hold_the_registry_lock() {
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let registry = REENTRANT_REGISTRY.get_or_init(ClaimRegistry::new);
let claim = registry.claim(ReentrantKey(1)).expect("first claim wins");
let rendered = format!("{registry:?}");
drop(claim);
let _ = tx.send((rendered, registry.is_empty()));
});
let (rendered, drained) = recv_or_panic(&rx);
assert!(rendered.contains("ReentrantKey(1"), "{rendered}");
assert!(
rendered.contains("live=1"),
"the key set stays readable mid-format: {rendered}"
);
assert!(drained, "the claim released after the format");
}
#[test]
fn default_is_an_empty_registry() {
let registry: ClaimRegistry<u32> = ClaimRegistry::default();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[test]
fn recv_timeout_disconnected_is_reported_as_a_panic_not_a_deadlock() {
let (tx, rx) = mpsc::channel::<()>();
std::thread::spawn(move || {
let _tx = tx; panic!("simulated unrelated failure, e.g. `expect(\"first claim wins\")`");
});
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| recv_or_panic(&rx)));
let panic_payload = outcome.expect_err("recv_timeout on a disconnected channel panics");
let message = panic_payload
.downcast_ref::<&str>()
.copied()
.or_else(|| panic_payload.downcast_ref::<String>().map(String::as_str))
.expect("panic payload is a string");
assert!(
message.contains("panicked before sending"),
"a disconnected channel must be reported as a panic, not a deadlock: {message}"
);
assert!(
!message.contains("deadlocked"),
"a disconnected channel must not be misreported as a mutex deadlock: {message}"
);
}
}