1use anyhow::{Result, anyhow};
7use std::collections::{HashMap, HashSet};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11use tokio::sync::RwLock;
12
13const DEFAULT_LOCK_TIMEOUT_SECS: u64 = 300;
14const LOCK_POLL_INTERVAL_MS: u64 = 50;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum LockType {
19 Read,
21 Write,
23}
24
25#[derive(Debug, Clone)]
27pub struct LockInfo {
28 pub agent_id: String,
30 pub lock_type: LockType,
32 pub acquired_at: Instant,
34 pub timeout: Option<Duration>,
36}
37
38impl LockInfo {
39 pub fn is_expired(&self) -> bool {
41 if let Some(timeout) = self.timeout {
42 self.acquired_at.elapsed() > timeout
43 } else {
44 false
45 }
46 }
47
48 pub fn time_remaining(&self) -> Option<Duration> {
50 self.timeout.map(|timeout| {
51 let elapsed = self.acquired_at.elapsed();
52 if elapsed >= timeout {
53 Duration::ZERO
54 } else {
55 timeout - elapsed
56 }
57 })
58 }
59}
60
61#[derive(Debug, Clone, Default)]
63struct FileLockState {
64 write_lock: Option<LockInfo>,
66 read_locks: Vec<LockInfo>,
68}
69
70pub struct LockGuard {
72 manager: Arc<FileLockManager>,
73 agent_id: String,
74 path: PathBuf,
75 lock_type: LockType,
76}
77
78impl std::fmt::Debug for LockGuard {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("LockGuard")
81 .field("agent_id", &self.agent_id)
82 .field("path", &self.path)
83 .field("lock_type", &self.lock_type)
84 .finish()
85 }
86}
87
88impl Drop for LockGuard {
89 fn drop(&mut self) {
90 let manager = self.manager.clone();
92 let agent_id = self.agent_id.clone();
93 let path = self.path.clone();
94 let lock_type = self.lock_type;
95
96 tokio::spawn(async move {
98 if let Err(e) = manager
99 .release_lock_internal(&agent_id, &path, lock_type)
100 .await
101 {
102 tracing::warn!(
103 agent_id = %agent_id,
104 path = %path.display(),
105 ?lock_type,
106 error = %e,
107 "failed to release file lock on drop"
108 );
109 }
110 });
111 }
112}
113
114pub struct FileLockManager {
116 locks: RwLock<HashMap<PathBuf, FileLockState>>,
118 default_timeout: Option<Duration>,
120 waiting: RwLock<HashMap<String, HashSet<PathBuf>>>,
123}
124
125impl FileLockManager {
126 pub fn new() -> Self {
128 Self {
129 locks: RwLock::new(HashMap::new()),
130 default_timeout: Some(Duration::from_secs(DEFAULT_LOCK_TIMEOUT_SECS)),
131 waiting: RwLock::new(HashMap::new()),
132 }
133 }
134
135 pub fn with_timeout(timeout: Duration) -> Self {
137 Self {
138 locks: RwLock::new(HashMap::new()),
139 default_timeout: Some(timeout),
140 waiting: RwLock::new(HashMap::new()),
141 }
142 }
143
144 pub fn without_timeout() -> Self {
146 Self {
147 locks: RwLock::new(HashMap::new()),
148 default_timeout: None,
149 waiting: RwLock::new(HashMap::new()),
150 }
151 }
152
153 #[tracing::instrument(name = "agent.lock.acquire", skip_all, fields(agent_id, lock_type = ?lock_type))]
157 pub async fn acquire_lock(
158 self: &Arc<Self>,
159 agent_id: &str,
160 path: impl AsRef<Path>,
161 lock_type: LockType,
162 ) -> Result<LockGuard> {
163 self.acquire_lock_with_timeout(agent_id, path, lock_type, self.default_timeout)
164 .await
165 }
166
167 #[tracing::instrument(name = "agent.lock.acquire_timeout", skip_all, fields(agent_id, lock_type = ?lock_type))]
169 pub async fn acquire_lock_with_timeout(
170 self: &Arc<Self>,
171 agent_id: &str,
172 path: impl AsRef<Path>,
173 lock_type: LockType,
174 timeout: Option<Duration>,
175 ) -> Result<LockGuard> {
176 let path = path.as_ref().to_path_buf();
177 let mut locks = self.locks.write().await;
178
179 self.cleanup_expired_internal(&mut locks);
181
182 let state = locks.entry(path.clone()).or_default();
183
184 match lock_type {
185 LockType::Read => {
186 if let Some(write_lock) = &state.write_lock
188 && write_lock.agent_id != agent_id
189 {
190 return Err(anyhow!(
191 "File {} is write-locked by agent {}",
192 path.display(),
193 write_lock.agent_id
194 ));
195 }
196
197 state.read_locks.push(LockInfo {
199 agent_id: agent_id.to_string(),
200 lock_type: LockType::Read,
201 acquired_at: Instant::now(),
202 timeout,
203 });
204 }
205 LockType::Write => {
206 if let Some(write_lock) = &state.write_lock {
208 if write_lock.agent_id != agent_id {
209 return Err(anyhow!(
210 "File {} is already write-locked by agent {}",
211 path.display(),
212 write_lock.agent_id
213 ));
214 }
215 return Ok(LockGuard {
217 manager: Arc::clone(self),
218 agent_id: agent_id.to_string(),
219 path,
220 lock_type,
221 });
222 }
223
224 let other_readers: Vec<_> = state
226 .read_locks
227 .iter()
228 .filter(|lock| lock.agent_id != agent_id)
229 .map(|lock| lock.agent_id.clone())
230 .collect();
231
232 if !other_readers.is_empty() {
233 return Err(anyhow!(
234 "File {} has read locks from agents: {:?}",
235 path.display(),
236 other_readers
237 ));
238 }
239
240 state.write_lock = Some(LockInfo {
242 agent_id: agent_id.to_string(),
243 lock_type: LockType::Write,
244 acquired_at: Instant::now(),
245 timeout,
246 });
247 }
248 }
249
250 Ok(LockGuard {
251 manager: Arc::clone(self),
252 agent_id: agent_id.to_string(),
253 path,
254 lock_type,
255 })
256 }
257
258 #[tracing::instrument(name = "agent.lock.acquire_wait", skip_all, fields(agent_id, lock_type = ?lock_type))]
263 pub async fn acquire_with_wait(
264 self: &Arc<Self>,
265 agent_id: &str,
266 path: impl AsRef<Path>,
267 lock_type: LockType,
268 wait_timeout: Duration,
269 ) -> Result<LockGuard> {
270 let path = path.as_ref().to_path_buf();
271 let deadline = Instant::now() + wait_timeout;
272 let poll_interval = Duration::from_millis(LOCK_POLL_INTERVAL_MS);
273
274 loop {
275 if self.would_deadlock(agent_id, &path).await {
277 return Err(anyhow!(
278 "Deadlock detected: agent {} waiting for {} would create circular dependency",
279 agent_id,
280 path.display()
281 ));
282 }
283
284 match self
286 .acquire_lock_with_timeout(agent_id, &path, lock_type, self.default_timeout)
287 .await
288 {
289 Ok(guard) => {
290 self.stop_waiting(agent_id, &path).await;
292 return Ok(guard);
293 }
294 Err(_) if Instant::now() < deadline => {
295 self.start_waiting(agent_id, &path).await;
297
298 self.cleanup_expired().await;
300
301 tokio::time::sleep(poll_interval).await;
303 }
304 Err(e) => {
305 self.stop_waiting(agent_id, &path).await;
307 return Err(anyhow!(
308 "Lock acquisition timeout after {:?}: {}",
309 wait_timeout,
310 e
311 ));
312 }
313 }
314 }
315 }
316
317 async fn would_deadlock(&self, agent_id: &str, target_path: &Path) -> bool {
321 let locks = self.locks.read().await;
322 let waiting = self.waiting.read().await;
323
324 let current_holders = if let Some(state) = locks.get(target_path) {
326 let mut holders = HashSet::new();
327 if let Some(write_lock) = &state.write_lock {
328 holders.insert(write_lock.agent_id.clone());
329 }
330 for read_lock in &state.read_locks {
331 holders.insert(read_lock.agent_id.clone());
332 }
333 holders
334 } else {
335 return false; };
337
338 if current_holders.contains(agent_id) {
340 return false;
341 }
342
343 let mut visited = HashSet::new();
345 let mut stack = Vec::new();
346
347 for holder in current_holders {
348 stack.push(holder);
349 }
350
351 while let Some(current) = stack.pop() {
352 if current == agent_id {
353 return true; }
355
356 if visited.contains(¤t) {
357 continue;
358 }
359 visited.insert(current.clone());
360
361 if let Some(waiting_for) = waiting.get(¤t) {
363 for waiting_path in waiting_for {
365 if let Some(state) = locks.get(waiting_path) {
366 if let Some(write_lock) = &state.write_lock
367 && !visited.contains(&write_lock.agent_id)
368 {
369 stack.push(write_lock.agent_id.clone());
370 }
371 for read_lock in &state.read_locks {
372 if !visited.contains(&read_lock.agent_id) {
373 stack.push(read_lock.agent_id.clone());
374 }
375 }
376 }
377 }
378 }
379 }
380
381 false
382 }
383
384 async fn start_waiting(&self, agent_id: &str, path: &Path) {
386 let mut waiting = self.waiting.write().await;
387 waiting
388 .entry(agent_id.to_string())
389 .or_insert_with(HashSet::new)
390 .insert(path.to_path_buf());
391 }
392
393 async fn stop_waiting(&self, agent_id: &str, path: &Path) {
395 let mut waiting = self.waiting.write().await;
396 if let Some(paths) = waiting.get_mut(agent_id) {
397 paths.remove(path);
398 if paths.is_empty() {
399 waiting.remove(agent_id);
400 }
401 }
402 }
403
404 pub async fn clear_waiting(&self, agent_id: &str) {
406 let mut waiting = self.waiting.write().await;
407 waiting.remove(agent_id);
408 }
409
410 pub async fn get_waiting_agents(&self) -> HashMap<String, Vec<PathBuf>> {
412 let waiting = self.waiting.read().await;
413 waiting
414 .iter()
415 .map(|(k, v)| (k.clone(), v.iter().cloned().collect()))
416 .collect()
417 }
418
419 #[tracing::instrument(name = "agent.lock.release", skip_all, fields(agent_id, lock_type = ?lock_type))]
421 pub async fn release_lock(
422 &self,
423 agent_id: &str,
424 path: impl AsRef<Path>,
425 lock_type: LockType,
426 ) -> Result<()> {
427 self.release_lock_internal(agent_id, path.as_ref(), lock_type)
428 .await
429 }
430
431 async fn release_lock_internal(
433 &self,
434 agent_id: &str,
435 path: &Path,
436 lock_type: LockType,
437 ) -> Result<()> {
438 let mut locks = self.locks.write().await;
439
440 if let Some(state) = locks.get_mut(path) {
441 match lock_type {
442 LockType::Read => {
443 let original_len = state.read_locks.len();
445 state.read_locks.retain(|lock| lock.agent_id != agent_id);
446
447 if state.read_locks.len() == original_len {
448 return Err(anyhow!(
449 "No read lock found for agent {} on {}",
450 agent_id,
451 path.display()
452 ));
453 }
454 }
455 LockType::Write => {
456 if let Some(write_lock) = &state.write_lock {
458 if write_lock.agent_id == agent_id {
459 state.write_lock = None;
460 } else {
461 return Err(anyhow!(
462 "Write lock on {} belongs to agent {}, not {}",
463 path.display(),
464 write_lock.agent_id,
465 agent_id
466 ));
467 }
468 } else {
469 return Err(anyhow!("No write lock found on {}", path.display()));
470 }
471 }
472 }
473
474 if state.write_lock.is_none() && state.read_locks.is_empty() {
476 locks.remove(path);
477 }
478 } else {
479 return Err(anyhow!("No locks found for {}", path.display()));
480 }
481
482 Ok(())
483 }
484
485 #[tracing::instrument(name = "agent.lock.release_all", skip(self))]
487 pub async fn release_all_locks(&self, agent_id: &str) -> usize {
488 let mut locks = self.locks.write().await;
489 let mut released = 0;
490
491 for state in locks.values_mut() {
492 if let Some(write_lock) = &state.write_lock
494 && write_lock.agent_id == agent_id
495 {
496 state.write_lock = None;
497 released += 1;
498 }
499
500 let original_len = state.read_locks.len();
502 state.read_locks.retain(|lock| lock.agent_id != agent_id);
503 released += original_len - state.read_locks.len();
504 }
505
506 locks.retain(|_, state| state.write_lock.is_some() || !state.read_locks.is_empty());
508
509 released
510 }
511
512 pub async fn check_lock(&self, path: impl AsRef<Path>) -> Option<LockInfo> {
514 let locks = self.locks.read().await;
515
516 if let Some(state) = locks.get(path.as_ref()) {
517 if let Some(write_lock) = &state.write_lock {
519 return Some(write_lock.clone());
520 }
521 if let Some(read_lock) = state.read_locks.first() {
522 return Some(read_lock.clone());
523 }
524 }
525
526 None
527 }
528
529 pub async fn is_locked_by(&self, path: impl AsRef<Path>, agent_id: &str) -> bool {
531 let locks = self.locks.read().await;
532
533 if let Some(state) = locks.get(path.as_ref()) {
534 if let Some(write_lock) = &state.write_lock
535 && write_lock.agent_id == agent_id
536 {
537 return true;
538 }
539 if state
540 .read_locks
541 .iter()
542 .any(|lock| lock.agent_id == agent_id)
543 {
544 return true;
545 }
546 }
547
548 false
549 }
550
551 pub async fn can_acquire(
553 &self,
554 path: impl AsRef<Path>,
555 agent_id: &str,
556 lock_type: LockType,
557 ) -> bool {
558 let locks = self.locks.read().await;
559
560 if let Some(state) = locks.get(path.as_ref()) {
561 match lock_type {
562 LockType::Read => {
563 if let Some(write_lock) = &state.write_lock {
565 return write_lock.agent_id == agent_id;
566 }
567 true
568 }
569 LockType::Write => {
570 if let Some(write_lock) = &state.write_lock
572 && write_lock.agent_id != agent_id
573 {
574 return false;
575 }
576 !state
577 .read_locks
578 .iter()
579 .any(|lock| lock.agent_id != agent_id)
580 }
581 }
582 } else {
583 true
584 }
585 }
586
587 pub async fn force_release(&self, path: impl AsRef<Path>) -> Result<()> {
589 let mut locks = self.locks.write().await;
590
591 if locks.remove(path.as_ref()).is_some() {
592 Ok(())
593 } else {
594 Err(anyhow!("No locks found for {}", path.as_ref().display()))
595 }
596 }
597
598 pub async fn list_locks(&self) -> Vec<(PathBuf, LockInfo)> {
600 let locks = self.locks.read().await;
601 let mut result = Vec::new();
602
603 for (path, state) in locks.iter() {
604 if let Some(write_lock) = &state.write_lock {
605 result.push((path.clone(), write_lock.clone()));
606 }
607 for read_lock in &state.read_locks {
608 result.push((path.clone(), read_lock.clone()));
609 }
610 }
611
612 result
613 }
614
615 pub async fn locks_for_agent(&self, agent_id: &str) -> Vec<(PathBuf, LockInfo)> {
617 let locks = self.locks.read().await;
618 let mut result = Vec::new();
619
620 for (path, state) in locks.iter() {
621 if let Some(write_lock) = &state.write_lock
622 && write_lock.agent_id == agent_id
623 {
624 result.push((path.clone(), write_lock.clone()));
625 }
626 for read_lock in &state.read_locks {
627 if read_lock.agent_id == agent_id {
628 result.push((path.clone(), read_lock.clone()));
629 }
630 }
631 }
632
633 result
634 }
635
636 pub async fn cleanup_expired(&self) -> usize {
638 let mut locks = self.locks.write().await;
639 self.cleanup_expired_internal(&mut locks)
640 }
641
642 fn cleanup_expired_internal(&self, locks: &mut HashMap<PathBuf, FileLockState>) -> usize {
644 let mut cleaned = 0;
645
646 for state in locks.values_mut() {
647 if let Some(write_lock) = &state.write_lock
649 && write_lock.is_expired()
650 {
651 state.write_lock = None;
652 cleaned += 1;
653 }
654
655 let original_len = state.read_locks.len();
657 state.read_locks.retain(|lock| !lock.is_expired());
658 cleaned += original_len - state.read_locks.len();
659 }
660
661 locks.retain(|_, state| state.write_lock.is_some() || !state.read_locks.is_empty());
663
664 cleaned
665 }
666
667 pub async fn stats(&self) -> LockStats {
669 let locks = self.locks.read().await;
670
671 let mut total_files = 0;
672 let mut total_write_locks = 0;
673 let mut total_read_locks = 0;
674
675 for state in locks.values() {
676 total_files += 1;
677 if state.write_lock.is_some() {
678 total_write_locks += 1;
679 }
680 total_read_locks += state.read_locks.len();
681 }
682
683 LockStats {
684 total_files,
685 total_write_locks,
686 total_read_locks,
687 }
688 }
689}
690
691impl Default for FileLockManager {
692 fn default() -> Self {
693 Self::new()
694 }
695}
696
697#[derive(Debug, Clone)]
699pub struct LockStats {
700 pub total_files: usize,
702 pub total_write_locks: usize,
704 pub total_read_locks: usize,
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711
712 #[tokio::test]
713 async fn test_acquire_write_lock() {
714 let manager = Arc::new(FileLockManager::new());
715 let guard = manager
716 .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
717 .await
718 .unwrap();
719
720 assert_eq!(guard.lock_type, LockType::Write);
721 assert!(manager.is_locked_by("/test/file.txt", "agent-1").await);
722 }
723
724 #[tokio::test]
725 async fn test_acquire_read_lock() {
726 let manager = Arc::new(FileLockManager::new());
727 let _guard = manager
728 .acquire_lock("agent-1", "/test/file.txt", LockType::Read)
729 .await
730 .unwrap();
731
732 assert!(manager.is_locked_by("/test/file.txt", "agent-1").await);
733 }
734
735 #[tokio::test]
736 async fn test_multiple_read_locks() {
737 let manager = Arc::new(FileLockManager::new());
738
739 let _guard1 = manager
740 .acquire_lock("agent-1", "/test/file.txt", LockType::Read)
741 .await
742 .unwrap();
743 let _guard2 = manager
744 .acquire_lock("agent-2", "/test/file.txt", LockType::Read)
745 .await
746 .unwrap();
747
748 assert!(manager.is_locked_by("/test/file.txt", "agent-1").await);
749 assert!(manager.is_locked_by("/test/file.txt", "agent-2").await);
750 }
751
752 #[tokio::test]
753 async fn test_write_lock_blocks_other_write() {
754 let manager = Arc::new(FileLockManager::new());
755
756 let _guard = manager
757 .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
758 .await
759 .unwrap();
760
761 let result = manager
762 .acquire_lock("agent-2", "/test/file.txt", LockType::Write)
763 .await;
764
765 assert!(result.is_err());
766 }
767
768 #[tokio::test]
769 async fn test_write_lock_blocks_read() {
770 let manager = Arc::new(FileLockManager::new());
771
772 let _guard = manager
773 .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
774 .await
775 .unwrap();
776
777 let result = manager
778 .acquire_lock("agent-2", "/test/file.txt", LockType::Read)
779 .await;
780
781 assert!(result.is_err());
782 }
783
784 #[tokio::test]
785 async fn test_read_lock_blocks_write() {
786 let manager = Arc::new(FileLockManager::new());
787
788 let _guard = manager
789 .acquire_lock("agent-1", "/test/file.txt", LockType::Read)
790 .await
791 .unwrap();
792
793 let result = manager
794 .acquire_lock("agent-2", "/test/file.txt", LockType::Write)
795 .await;
796
797 assert!(result.is_err());
798 }
799
800 #[tokio::test]
801 async fn test_same_agent_reacquire_write() {
802 let manager = Arc::new(FileLockManager::new());
803
804 let _guard1 = manager
805 .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
806 .await
807 .unwrap();
808 let _guard2 = manager
809 .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
810 .await
811 .unwrap();
812
813 assert!(manager.is_locked_by("/test/file.txt", "agent-1").await);
815 }
816
817 #[tokio::test]
818 async fn test_release_all_locks() {
819 let manager = Arc::new(FileLockManager::new());
820
821 let _guard1 = manager
822 .acquire_lock("agent-1", "/test/file1.txt", LockType::Write)
823 .await
824 .unwrap();
825 let _guard2 = manager
826 .acquire_lock("agent-1", "/test/file2.txt", LockType::Read)
827 .await
828 .unwrap();
829
830 std::mem::forget(_guard1);
832 std::mem::forget(_guard2);
833
834 let released = manager.release_all_locks("agent-1").await;
835 assert_eq!(released, 2);
836 }
837
838 #[tokio::test]
839 async fn test_lock_stats() {
840 let manager = Arc::new(FileLockManager::new());
841
842 let _guard1 = manager
843 .acquire_lock("agent-1", "/test/file1.txt", LockType::Write)
844 .await
845 .unwrap();
846 let _guard2 = manager
847 .acquire_lock("agent-2", "/test/file2.txt", LockType::Read)
848 .await
849 .unwrap();
850 let _guard3 = manager
851 .acquire_lock("agent-3", "/test/file2.txt", LockType::Read)
852 .await
853 .unwrap();
854
855 let stats = manager.stats().await;
856 assert_eq!(stats.total_files, 2);
857 assert_eq!(stats.total_write_locks, 1);
858 assert_eq!(stats.total_read_locks, 2);
859 }
860
861 #[tokio::test]
862 async fn test_can_acquire() {
863 let manager = Arc::new(FileLockManager::new());
864
865 assert!(
867 manager
868 .can_acquire("/test/file.txt", "agent-1", LockType::Write)
869 .await
870 );
871 assert!(
872 manager
873 .can_acquire("/test/file.txt", "agent-1", LockType::Read)
874 .await
875 );
876
877 let _guard = manager
878 .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
879 .await
880 .unwrap();
881
882 assert!(
884 manager
885 .can_acquire("/test/file.txt", "agent-1", LockType::Write)
886 .await
887 );
888 assert!(
889 manager
890 .can_acquire("/test/file.txt", "agent-1", LockType::Read)
891 .await
892 );
893
894 assert!(
896 !manager
897 .can_acquire("/test/file.txt", "agent-2", LockType::Write)
898 .await
899 );
900 assert!(
901 !manager
902 .can_acquire("/test/file.txt", "agent-2", LockType::Read)
903 .await
904 );
905 }
906
907 #[tokio::test]
908 async fn test_expired_lock_cleanup() {
909 let manager = Arc::new(FileLockManager::new());
910
911 let _guard = manager
913 .acquire_lock_with_timeout(
914 "agent-1",
915 "/test/file.txt",
916 LockType::Write,
917 Some(Duration::from_millis(1)),
918 )
919 .await
920 .unwrap();
921
922 std::mem::forget(_guard);
924
925 tokio::time::sleep(Duration::from_millis(10)).await;
927
928 let cleaned = manager.cleanup_expired().await;
930 assert_eq!(cleaned, 1);
931
932 let result = manager
934 .acquire_lock("agent-2", "/test/file.txt", LockType::Write)
935 .await;
936 assert!(result.is_ok());
937 }
938
939 #[tokio::test]
940 async fn test_force_release() {
941 let manager = Arc::new(FileLockManager::new());
942
943 let _guard = manager
944 .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
945 .await
946 .unwrap();
947
948 std::mem::forget(_guard);
950
951 manager.force_release("/test/file.txt").await.unwrap();
953
954 let result = manager
956 .acquire_lock("agent-2", "/test/file.txt", LockType::Write)
957 .await;
958 assert!(result.is_ok());
959 }
960
961 #[tokio::test]
962 async fn test_list_locks() {
963 let manager = Arc::new(FileLockManager::new());
964
965 let _guard1 = manager
966 .acquire_lock("agent-1", "/test/file1.txt", LockType::Write)
967 .await
968 .unwrap();
969 let _guard2 = manager
970 .acquire_lock("agent-2", "/test/file2.txt", LockType::Read)
971 .await
972 .unwrap();
973
974 let locks = manager.list_locks().await;
975 assert_eq!(locks.len(), 2);
976 }
977
978 #[tokio::test]
979 async fn test_locks_for_agent() {
980 let manager = Arc::new(FileLockManager::new());
981
982 let _guard1 = manager
983 .acquire_lock("agent-1", "/test/file1.txt", LockType::Write)
984 .await
985 .unwrap();
986 let _guard2 = manager
987 .acquire_lock("agent-1", "/test/file2.txt", LockType::Read)
988 .await
989 .unwrap();
990 let _guard3 = manager
991 .acquire_lock("agent-2", "/test/file3.txt", LockType::Write)
992 .await
993 .unwrap();
994
995 let agent1_locks = manager.locks_for_agent("agent-1").await;
996 assert_eq!(agent1_locks.len(), 2);
997
998 let agent2_locks = manager.locks_for_agent("agent-2").await;
999 assert_eq!(agent2_locks.len(), 1);
1000 }
1001
1002 #[tokio::test]
1003 async fn test_acquire_with_wait_success() {
1004 let manager = Arc::new(FileLockManager::new());
1005
1006 let guard = manager
1008 .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
1009 .await
1010 .unwrap();
1011
1012 let manager_clone = manager.clone();
1014 tokio::spawn(async move {
1015 tokio::time::sleep(Duration::from_millis(50)).await;
1016 drop(guard);
1017 tokio::time::sleep(Duration::from_millis(10)).await;
1019 manager_clone.cleanup_expired().await;
1020 });
1021
1022 let result = manager
1024 .acquire_with_wait(
1025 "agent-2",
1026 "/test/file.txt",
1027 LockType::Write,
1028 Duration::from_millis(500),
1029 )
1030 .await;
1031
1032 assert!(result.is_ok());
1033 }
1034
1035 #[tokio::test]
1036 async fn test_acquire_with_wait_timeout() {
1037 let manager = Arc::new(FileLockManager::new());
1038
1039 let _guard = manager
1041 .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
1042 .await
1043 .unwrap();
1044
1045 let result = manager
1047 .acquire_with_wait(
1048 "agent-2",
1049 "/test/file.txt",
1050 LockType::Write,
1051 Duration::from_millis(100),
1052 )
1053 .await;
1054
1055 assert!(result.is_err());
1056 assert!(result.unwrap_err().to_string().contains("timeout"));
1057 }
1058
1059 #[tokio::test]
1060 async fn test_deadlock_detection() {
1061 let manager = Arc::new(FileLockManager::new());
1062
1063 let _guard1 = manager
1065 .acquire_lock("agent-1", "/test/file1.txt", LockType::Write)
1066 .await
1067 .unwrap();
1068
1069 let _guard2 = manager
1071 .acquire_lock("agent-2", "/test/file2.txt", LockType::Write)
1072 .await
1073 .unwrap();
1074
1075 manager
1077 .start_waiting("agent-1", std::path::Path::new("/test/file2.txt"))
1078 .await;
1079
1080 assert!(
1082 manager
1083 .would_deadlock("agent-2", std::path::Path::new("/test/file1.txt"))
1084 .await
1085 );
1086
1087 assert!(
1089 !manager
1090 .would_deadlock("agent-3", std::path::Path::new("/test/file1.txt"))
1091 .await
1092 );
1093 }
1094
1095 #[tokio::test]
1096 async fn test_waiting_agents() {
1097 let manager = Arc::new(FileLockManager::new());
1098
1099 manager
1101 .start_waiting("agent-1", std::path::Path::new("/test/file1.txt"))
1102 .await;
1103 manager
1104 .start_waiting("agent-1", std::path::Path::new("/test/file2.txt"))
1105 .await;
1106 manager
1107 .start_waiting("agent-2", std::path::Path::new("/test/file1.txt"))
1108 .await;
1109
1110 let waiting = manager.get_waiting_agents().await;
1111 assert_eq!(waiting.len(), 2);
1112 assert_eq!(waiting.get("agent-1").map(|v| v.len()), Some(2));
1113 assert_eq!(waiting.get("agent-2").map(|v| v.len()), Some(1));
1114
1115 manager.clear_waiting("agent-1").await;
1117
1118 let waiting = manager.get_waiting_agents().await;
1119 assert_eq!(waiting.len(), 1);
1120 assert!(!waiting.contains_key("agent-1"));
1121 }
1122}