1use std::collections::HashMap;
2use std::sync::Arc;
3use tokio::sync::Mutex;
4
5#[derive(Clone, Default)]
8pub struct WorkspaceLockManager {
9 locks: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
10}
11
12impl WorkspaceLockManager {
13 pub fn new() -> Self {
14 Self::default()
15 }
16
17 pub async fn get_lock(&self, project: &str) -> Arc<Mutex<()>> {
19 let mut map = self.locks.lock().await;
20 map.entry(project.to_string())
21 .or_insert_with(|| Arc::new(Mutex::new(())))
22 .clone()
23 }
24
25 pub async fn is_locked(&self, project: &str) -> bool {
30 let map = self.locks.lock().await;
31 map.get(project)
32 .map(|m| m.try_lock().is_err())
33 .unwrap_or(false)
34 }
35
36 pub async fn locked_projects(&self) -> Vec<String> {
40 let map = self.locks.lock().await;
41 map.iter()
42 .filter(|(_, mutex)| mutex.try_lock().is_err())
43 .map(|(name, _)| name.clone())
44 .collect()
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 #[tokio::test]
53 async fn test_workspace_lock_manager_same_project_shares_mutex() {
54 let manager = WorkspaceLockManager::new();
55 let lock1 = manager.get_lock("my-project").await;
56 let lock2 = manager.get_lock("my-project").await;
57
58 let guard1 = lock1.try_lock();
60 assert!(guard1.is_ok());
61
62 let guard2 = lock2.try_lock();
64 assert!(guard2.is_err());
65
66 drop(guard1);
68 let guard2_retry = lock2.try_lock();
69 assert!(guard2_retry.is_ok());
70 }
71
72 #[tokio::test]
73 async fn test_workspace_lock_manager_different_projects_independent() {
74 let manager = WorkspaceLockManager::new();
75 let lock1 = manager.get_lock("project-a").await;
76 let lock2 = manager.get_lock("project-b").await;
77
78 let guard1 = lock1.try_lock();
79 let guard2 = lock2.try_lock();
80
81 assert!(guard1.is_ok());
82 assert!(guard2.is_ok());
83 }
84
85 #[tokio::test]
86 async fn test_is_locked_reflects_held_locks_without_inserting() {
87 let manager = WorkspaceLockManager::new();
88
89 assert!(!manager.is_locked("ghost-project").await);
91
92 let lock = manager.get_lock("busy-project").await;
93 let guard = lock.try_lock().unwrap();
94 assert!(manager.is_locked("busy-project").await);
95
96 let locked = manager.locked_projects().await;
98 assert_eq!(locked, vec!["busy-project".to_string()]);
99
100 drop(guard);
101 assert!(!manager.is_locked("busy-project").await);
102 assert!(manager.locked_projects().await.is_empty());
103 }
104}