use std::collections::BTreeMap;
use haz_domain::mutex::{Mutex, MutexMode, MutexScope};
use haz_domain::name::{MutexName, ProjectName};
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum HoldKey {
Workspace {
name: MutexName,
},
Project {
project: ProjectName,
name: MutexName,
},
}
#[allow(dead_code)]
impl HoldKey {
pub(crate) fn from_task(project: &ProjectName, mutex: &Mutex) -> Self {
match mutex.scope {
MutexScope::Workspace => Self::Workspace {
name: mutex.name.clone(),
},
MutexScope::Project => Self::Project {
project: project.clone(),
name: mutex.name.clone(),
},
}
}
}
#[allow(dead_code)]
#[derive(Debug, Default, Clone)]
pub(crate) struct HoldSet {
by_key: BTreeMap<HoldKey, Vec<MutexMode>>,
}
#[allow(dead_code)]
impl HoldSet {
pub(crate) fn compatible(
&self,
task_project: &ProjectName,
task_mutex: Option<&Mutex>,
) -> bool {
let Some(m) = task_mutex else {
return true;
};
let key = HoldKey::from_task(task_project, m);
let Some(holders) = self.by_key.get(&key) else {
return true;
};
if holders.is_empty() {
return true;
}
match m.mode {
MutexMode::Exclusive => false,
MutexMode::Shared => holders.iter().all(|h| *h == MutexMode::Shared),
}
}
pub(crate) fn acquire(&mut self, task_project: &ProjectName, task_mutex: Option<&Mutex>) {
let Some(m) = task_mutex else {
return;
};
let key = HoldKey::from_task(task_project, m);
self.by_key.entry(key).or_default().push(m.mode);
}
pub(crate) fn release(&mut self, task_project: &ProjectName, task_mutex: Option<&Mutex>) {
let Some(m) = task_mutex else {
return;
};
let key = HoldKey::from_task(task_project, m);
if let Some(holders) = self.by_key.get_mut(&key)
&& let Some(pos) = holders.iter().position(|h| *h == m.mode)
{
holders.swap_remove(pos);
if holders.is_empty() {
self.by_key.remove(&key);
}
}
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use haz_domain::mutex::{Mutex, MutexMode, MutexScope};
use haz_domain::name::{MutexName, ProjectName};
use super::{HoldKey, HoldSet};
fn project(s: &str) -> ProjectName {
ProjectName::from_str(s).unwrap()
}
fn mname(s: &str) -> MutexName {
MutexName::from_str(s).unwrap()
}
fn workspace_mutex(name: &str, mode: MutexMode) -> Mutex {
Mutex {
scope: MutexScope::Workspace,
name: mname(name),
mode,
}
}
fn project_mutex(name: &str, mode: MutexMode) -> Mutex {
Mutex {
scope: MutexScope::Project,
name: mname(name),
mode,
}
}
#[test]
fn hold_key_from_task_workspace_scope() {
let p = project("p1");
let m = workspace_mutex("db", MutexMode::Exclusive);
assert_eq!(
HoldKey::from_task(&p, &m),
HoldKey::Workspace { name: mname("db") },
);
}
#[test]
fn hold_key_from_task_project_scope_binds_bearing_project() {
let p = project("p1");
let m = project_mutex("db", MutexMode::Exclusive);
assert_eq!(
HoldKey::from_task(&p, &m),
HoldKey::Project {
project: project("p1"),
name: mname("db"),
},
);
}
#[test]
fn workspace_and_project_scope_same_name_are_distinct_keys() {
let p = project("p1");
let w = workspace_mutex("db", MutexMode::Exclusive);
let pr = project_mutex("db", MutexMode::Exclusive);
assert_ne!(HoldKey::from_task(&p, &w), HoldKey::from_task(&p, &pr));
}
#[test]
fn project_scope_same_name_different_projects_are_distinct_keys() {
let m = project_mutex("db", MutexMode::Exclusive);
assert_ne!(
HoldKey::from_task(&project("p1"), &m),
HoldKey::from_task(&project("p2"), &m),
);
}
#[test]
fn workspace_scope_ignores_bearing_project() {
let m = workspace_mutex("db", MutexMode::Exclusive);
assert_eq!(
HoldKey::from_task(&project("p1"), &m),
HoldKey::from_task(&project("p2"), &m),
);
}
#[test]
fn empty_set_compatible_with_none_mutex() {
let hs = HoldSet::default();
assert!(hs.compatible(&project("p"), None));
}
#[test]
fn empty_set_compatible_with_workspace_exclusive() {
let hs = HoldSet::default();
let m = workspace_mutex("db", MutexMode::Exclusive);
assert!(hs.compatible(&project("p"), Some(&m)));
}
#[test]
fn empty_set_compatible_with_workspace_shared() {
let hs = HoldSet::default();
let m = workspace_mutex("db", MutexMode::Shared);
assert!(hs.compatible(&project("p"), Some(&m)));
}
#[test]
fn empty_set_compatible_with_project_exclusive() {
let hs = HoldSet::default();
let m = project_mutex("db", MutexMode::Exclusive);
assert!(hs.compatible(&project("p"), Some(&m)));
}
#[test]
fn empty_set_compatible_with_project_shared() {
let hs = HoldSet::default();
let m = project_mutex("db", MutexMode::Shared);
assert!(hs.compatible(&project("p"), Some(&m)));
}
#[test]
fn workspace_db_does_not_block_project_db_in_same_project() {
let mut hs = HoldSet::default();
let w = workspace_mutex("db", MutexMode::Exclusive);
hs.acquire(&project("p1"), Some(&w));
let pr = project_mutex("db", MutexMode::Exclusive);
assert!(hs.compatible(&project("p1"), Some(&pr)));
}
#[test]
fn workspace_db_held_blocks_workspace_db_in_any_project() {
let mut hs = HoldSet::default();
let m = workspace_mutex("db", MutexMode::Exclusive);
hs.acquire(&project("p1"), Some(&m));
assert!(!hs.compatible(&project("p2"), Some(&m)));
}
#[test]
fn project_db_in_p1_does_not_block_project_db_in_p2() {
let mut hs = HoldSet::default();
let m = project_mutex("db", MutexMode::Exclusive);
hs.acquire(&project("p1"), Some(&m));
assert!(hs.compatible(&project("p2"), Some(&m)));
}
#[test]
fn mutex_003_exclusive_blocks_incoming_exclusive() {
let mut hs = HoldSet::default();
let m = workspace_mutex("db", MutexMode::Exclusive);
hs.acquire(&project("p"), Some(&m));
assert!(!hs.compatible(&project("p"), Some(&m)));
}
#[test]
fn mutex_003_exclusive_blocks_incoming_shared() {
let mut hs = HoldSet::default();
let excl = workspace_mutex("db", MutexMode::Exclusive);
let shrd = workspace_mutex("db", MutexMode::Shared);
hs.acquire(&project("p"), Some(&excl));
assert!(!hs.compatible(&project("p"), Some(&shrd)));
}
#[test]
fn mutex_004_shared_blocks_incoming_exclusive() {
let mut hs = HoldSet::default();
let shrd = workspace_mutex("db", MutexMode::Shared);
let excl = workspace_mutex("db", MutexMode::Exclusive);
hs.acquire(&project("p"), Some(&shrd));
assert!(!hs.compatible(&project("p"), Some(&excl)));
}
#[test]
fn mutex_004_shared_compatible_with_shared() {
let mut hs = HoldSet::default();
let shrd = workspace_mutex("db", MutexMode::Shared);
hs.acquire(&project("p"), Some(&shrd));
assert!(hs.compatible(&project("p"), Some(&shrd)));
}
#[test]
fn mutex_004_many_shared_holders_still_block_exclusive() {
let mut hs = HoldSet::default();
let shrd = workspace_mutex("db", MutexMode::Shared);
let excl = workspace_mutex("db", MutexMode::Exclusive);
hs.acquire(&project("p"), Some(&shrd));
hs.acquire(&project("p"), Some(&shrd));
hs.acquire(&project("p"), Some(&shrd));
assert!(!hs.compatible(&project("p"), Some(&excl)));
}
#[test]
fn mutex_006_release_decrements_shared_count() {
let mut hs = HoldSet::default();
let shrd = workspace_mutex("db", MutexMode::Shared);
hs.acquire(&project("p"), Some(&shrd));
hs.acquire(&project("p"), Some(&shrd));
hs.release(&project("p"), Some(&shrd));
let excl = workspace_mutex("db", MutexMode::Exclusive);
assert!(!hs.compatible(&project("p"), Some(&excl)));
}
#[test]
fn mutex_006_release_full_set_makes_exclusive_compatible() {
let mut hs = HoldSet::default();
let shrd = workspace_mutex("db", MutexMode::Shared);
hs.acquire(&project("p"), Some(&shrd));
hs.release(&project("p"), Some(&shrd));
let excl = workspace_mutex("db", MutexMode::Exclusive);
assert!(hs.compatible(&project("p"), Some(&excl)));
}
#[test]
fn mutex_006_release_one_of_two_shared_leaves_exclusive_blocked() {
let mut hs = HoldSet::default();
let shrd = workspace_mutex("db", MutexMode::Shared);
hs.acquire(&project("p"), Some(&shrd));
hs.acquire(&project("p"), Some(&shrd));
hs.release(&project("p"), Some(&shrd));
let excl = workspace_mutex("db", MutexMode::Exclusive);
assert!(!hs.compatible(&project("p"), Some(&excl)));
}
#[test]
fn mutex_006_acquire_then_release_exclusive_round_trip() {
let mut hs = HoldSet::default();
let excl = workspace_mutex("db", MutexMode::Exclusive);
hs.acquire(&project("p"), Some(&excl));
assert!(!hs.compatible(&project("p"), Some(&excl)));
hs.release(&project("p"), Some(&excl));
assert!(hs.compatible(&project("p"), Some(&excl)));
}
#[test]
fn none_mutex_acquire_is_noop() {
let mut hs = HoldSet::default();
hs.acquire(&project("p"), None);
let m = workspace_mutex("db", MutexMode::Exclusive);
assert!(hs.compatible(&project("p"), Some(&m)));
}
#[test]
fn none_mutex_release_is_noop() {
let mut hs = HoldSet::default();
hs.release(&project("p"), None);
let m = workspace_mutex("db", MutexMode::Exclusive);
assert!(hs.compatible(&project("p"), Some(&m)));
}
#[test]
fn release_of_nonexistent_key_is_noop() {
let mut hs = HoldSet::default();
let m = workspace_mutex("db", MutexMode::Exclusive);
hs.release(&project("p"), Some(&m));
assert!(hs.compatible(&project("p"), Some(&m)));
}
#[test]
fn release_more_than_acquired_does_not_underflow() {
let mut hs = HoldSet::default();
let m = workspace_mutex("db", MutexMode::Exclusive);
hs.acquire(&project("p"), Some(&m));
hs.release(&project("p"), Some(&m));
hs.release(&project("p"), Some(&m));
assert!(hs.compatible(&project("p"), Some(&m)));
hs.acquire(&project("p"), Some(&m));
assert!(!hs.compatible(&project("p"), Some(&m)));
}
#[test]
fn workspace_held_does_not_block_project_scope_same_name() {
let mut hs = HoldSet::default();
let w = workspace_mutex("db", MutexMode::Exclusive);
let pr = project_mutex("db", MutexMode::Exclusive);
hs.acquire(&project("p"), Some(&w));
assert!(hs.compatible(&project("p"), Some(&pr)));
hs.acquire(&project("p"), Some(&pr));
hs.release(&project("p"), Some(&w));
assert!(!hs.compatible(&project("p"), Some(&pr)));
}
}