use std::collections::HashSet;
use std::sync::Arc;
use parking_lot::Mutex;
use crate::catalog::{DatabaseId, NamespaceId};
use crate::val::TableName;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct TableKey {
ns: NamespaceId,
db: DatabaseId,
table: TableName,
}
#[derive(Clone, Default)]
pub(crate) struct CycleGuard {
inner: Arc<Mutex<HashSet<TableKey>>>,
}
impl CycleGuard {
#[must_use]
pub(crate) fn try_enter(
&self,
ns: NamespaceId,
db: DatabaseId,
table: TableName,
) -> Option<CycleGuardEntry> {
let key = TableKey {
ns,
db,
table,
};
let mut guard = self.inner.lock();
if guard.contains(&key) {
None
} else {
guard.insert(key.clone());
Some(CycleGuardEntry {
guard: Arc::clone(&self.inner),
key,
})
}
}
#[cfg(test)]
fn contains(&self, ns: NamespaceId, db: DatabaseId, table: &TableName) -> bool {
self.inner.lock().contains(&TableKey {
ns,
db,
table: table.clone(),
})
}
}
pub(crate) struct CycleGuardEntry {
guard: Arc<Mutex<HashSet<TableKey>>>,
key: TableKey,
}
impl Drop for CycleGuardEntry {
fn drop(&mut self) {
self.guard.lock().remove(&self.key);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::val::TableName;
fn key(name: &str) -> (NamespaceId, DatabaseId, TableName) {
(NamespaceId(1), DatabaseId(2), TableName::new(name))
}
#[test]
fn empty_guard_does_not_contain_anything() {
let g = CycleGuard::default();
let (ns, db, t) = key("foo");
assert!(!g.contains(ns, db, &t));
}
#[test]
fn try_enter_succeeds_on_empty_guard() {
let g = CycleGuard::default();
let (ns, db, t) = key("foo");
let entry = g.try_enter(ns, db, t.clone());
assert!(entry.is_some());
assert!(g.contains(ns, db, &t));
}
#[test]
fn try_enter_fails_when_key_present() {
let g = CycleGuard::default();
let (ns, db, t) = key("foo");
let _outer = g.try_enter(ns, db, t.clone()).expect("first push must succeed");
let inner = g.try_enter(ns, db, t);
assert!(inner.is_none(), "second push of same key must return None");
}
#[test]
fn drop_removes_entry() {
let g = CycleGuard::default();
let (ns, db, t) = key("foo");
{
let _entry = g.try_enter(ns, db, t.clone()).expect("push");
assert!(g.contains(ns, db, &t));
}
assert!(!g.contains(ns, db, &t), "entry should be removed when guard drops");
}
#[test]
fn distinct_keys_coexist() {
let g = CycleGuard::default();
let (ns, db, t1) = key("foo");
let (_, _, t2) = key("bar");
let _e1 = g.try_enter(ns, db, t1.clone()).expect("push t1");
let _e2 = g.try_enter(ns, db, t2.clone()).expect("push t2");
assert!(g.contains(ns, db, &t1));
assert!(g.contains(ns, db, &t2));
}
#[test]
fn re_entry_after_drop_succeeds() {
let g = CycleGuard::default();
let (ns, db, t) = key("foo");
drop(g.try_enter(ns, db, t.clone()).expect("first push"));
let again = g.try_enter(ns, db, t);
assert!(again.is_some(), "re-entry after drop should succeed");
}
#[test]
fn cloned_guards_share_state() {
let g1 = CycleGuard::default();
let g2 = g1.clone();
let (ns, db, t) = key("foo");
let _entry = g1.try_enter(ns, db, t.clone()).expect("push via g1");
assert!(g2.contains(ns, db, &t), "g2 should see the key pushed via g1");
assert!(g2.try_enter(ns, db, t).is_none(), "g2's try_enter for the same key must fail");
}
#[test]
fn cross_database_keys_are_distinct() {
let g = CycleGuard::default();
let t = TableName::new("foo");
let _e1 = g.try_enter(NamespaceId(1), DatabaseId(2), t.clone()).expect("(ns=1, db=2)");
let e2 = g.try_enter(NamespaceId(1), DatabaseId(3), t);
assert!(e2.is_some(), "same table name in different db must not collide");
}
}