Skip to main content

rullama_agent/
file_locks.rs

1//! File locking system for multi-agent coordination
2//!
3//! Provides a mechanism for agents to "checkout" files, preventing concurrent
4//! modifications and ensuring consistency across background task agents.
5
6use 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/// Type of file lock
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum LockType {
19    /// Shared read lock - multiple agents can hold simultaneously
20    Read,
21    /// Exclusive write lock - only one agent can hold
22    Write,
23}
24
25/// Information about a held lock
26#[derive(Debug, Clone)]
27pub struct LockInfo {
28    /// ID of the agent holding the lock
29    pub agent_id: String,
30    /// Type of lock
31    pub lock_type: LockType,
32    /// When the lock was acquired
33    pub acquired_at: Instant,
34    /// Optional timeout for auto-release
35    pub timeout: Option<Duration>,
36}
37
38impl LockInfo {
39    /// Check if the lock has expired
40    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    /// Get remaining time before timeout
49    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/// Internal lock state for a file
62#[derive(Debug, Clone, Default)]
63struct FileLockState {
64    /// Write lock (exclusive)
65    write_lock: Option<LockInfo>,
66    /// Read locks (shared)
67    read_locks: Vec<LockInfo>,
68}
69
70/// Guard that releases a lock when dropped
71pub 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        // Use blocking release since we're in Drop
91        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        // Spawn a task to release the lock asynchronously
97        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
114/// Manages file locks across multiple agents
115pub struct FileLockManager {
116    /// Map of file paths to their lock states
117    locks: RwLock<HashMap<PathBuf, FileLockState>>,
118    /// Default timeout for locks
119    default_timeout: Option<Duration>,
120    /// Waiting agents: agent_id -> set of paths they're waiting for
121    /// Used for deadlock detection
122    waiting: RwLock<HashMap<String, HashSet<PathBuf>>>,
123}
124
125impl FileLockManager {
126    /// Create a new file lock manager
127    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    /// Create a file lock manager with a custom default timeout
136    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    /// Create a file lock manager with no default timeout
145    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    /// Acquire a lock on a file
154    ///
155    /// Returns a LockGuard that automatically releases the lock when dropped.
156    #[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    /// Acquire a lock with a specific timeout
168    #[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        // Clean up expired locks first
180        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                // Check for write lock
187                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                // Add read lock
198                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                // Check for existing write lock
207                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                    // Same agent already has write lock, return success
216                    return Ok(LockGuard {
217                        manager: Arc::clone(self),
218                        agent_id: agent_id.to_string(),
219                        path,
220                        lock_type,
221                    });
222                }
223
224                // Check for read locks by other agents
225                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                // Set write lock
241                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    /// Acquire a lock with waiting and timeout
259    ///
260    /// This method will wait up to `wait_timeout` for the lock to become available.
261    /// It includes deadlock detection to prevent circular wait scenarios.
262    #[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            // Check for deadlock before waiting
276            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            // Try to acquire the lock
285            match self
286                .acquire_lock_with_timeout(agent_id, &path, lock_type, self.default_timeout)
287                .await
288            {
289                Ok(guard) => {
290                    // Successfully acquired - remove from waiting set
291                    self.stop_waiting(agent_id, &path).await;
292                    return Ok(guard);
293                }
294                Err(_) if Instant::now() < deadline => {
295                    // Record that we're waiting for this path
296                    self.start_waiting(agent_id, &path).await;
297
298                    // Clean up expired locks that might be blocking us
299                    self.cleanup_expired().await;
300
301                    // Wait before retrying
302                    tokio::time::sleep(poll_interval).await;
303                }
304                Err(e) => {
305                    // Timeout or other error
306                    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    /// Check if acquiring a lock would cause a deadlock
318    ///
319    /// Uses cycle detection in the wait-for graph.
320    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        // Find who currently holds the lock on target_path
325        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; // No one holds the lock
336        };
337
338        // If we already hold the lock, no deadlock
339        if current_holders.contains(agent_id) {
340            return false;
341        }
342
343        // DFS to detect cycle: would any holder eventually wait for us?
344        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; // Cycle detected
354            }
355
356            if visited.contains(&current) {
357                continue;
358            }
359            visited.insert(current.clone());
360
361            // Find what paths this agent is waiting for
362            if let Some(waiting_for) = waiting.get(&current) {
363                // Find who holds those paths
364                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    /// Record that an agent is waiting for a path
385    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    /// Remove an agent from the waiting set for a path
394    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    /// Clear all waiting entries for an agent (e.g., when agent exits)
405    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    /// Get all agents currently waiting for locks
411    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    /// Release a specific lock
420    #[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    /// Internal release implementation
432    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                    // Remove matching read lock
444                    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                    // Remove write lock if it belongs to this agent
457                    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            // Clean up empty state
475            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    /// Release all locks held by an agent
486    #[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            // Release write lock
493            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            // Release read locks
501            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        // Clean up empty entries
507        locks.retain(|_, state| state.write_lock.is_some() || !state.read_locks.is_empty());
508
509        released
510    }
511
512    /// Check if a file is locked
513    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            // Return write lock if present, otherwise first read lock
518            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    /// Check if a file is locked by a specific agent
530    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    /// Check if a file can be locked with a specific type by an agent
552    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                    // Can read if no write lock or own write lock
564                    if let Some(write_lock) = &state.write_lock {
565                        return write_lock.agent_id == agent_id;
566                    }
567                    true
568                }
569                LockType::Write => {
570                    // Can write if no other agent has any lock
571                    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    /// Force release a lock (admin operation)
588    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    /// Get all currently held locks
599    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    /// Get locks held by a specific agent
616    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    /// Clean up expired locks
637    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    /// Internal cleanup implementation
643    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            // Clean expired write lock
648            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            // Clean expired read locks
656            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        // Remove empty entries
662        locks.retain(|_, state| state.write_lock.is_some() || !state.read_locks.is_empty());
663
664        cleaned
665    }
666
667    /// Get statistics about current locks
668    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/// Statistics about current locks
698#[derive(Debug, Clone)]
699pub struct LockStats {
700    /// Number of files with locks
701    pub total_files: usize,
702    /// Number of write locks
703    pub total_write_locks: usize,
704    /// Number of read locks
705    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        // Same agent can reacquire their own write lock
814        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        // Forget guards to prevent auto-release
831        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        // No locks - can acquire anything
866        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        // Same agent can acquire
883        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        // Other agent cannot
895        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        // Acquire lock with very short timeout
912        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        // Forget guard to prevent auto-release
923        std::mem::forget(_guard);
924
925        // Wait for expiration
926        tokio::time::sleep(Duration::from_millis(10)).await;
927
928        // Cleanup should remove expired lock
929        let cleaned = manager.cleanup_expired().await;
930        assert_eq!(cleaned, 1);
931
932        // Now another agent can acquire
933        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        // Forget guard
949        std::mem::forget(_guard);
950
951        // Force release
952        manager.force_release("/test/file.txt").await.unwrap();
953
954        // Another agent can now acquire
955        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        // Acquire initial lock
1007        let guard = manager
1008            .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
1009            .await
1010            .unwrap();
1011
1012        // Spawn task to release after delay
1013        let manager_clone = manager.clone();
1014        tokio::spawn(async move {
1015            tokio::time::sleep(Duration::from_millis(50)).await;
1016            drop(guard);
1017            // Give time for the drop to process
1018            tokio::time::sleep(Duration::from_millis(10)).await;
1019            manager_clone.cleanup_expired().await;
1020        });
1021
1022        // Agent 2 should wait and eventually acquire
1023        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        // Acquire lock that won't be released
1040        let _guard = manager
1041            .acquire_lock("agent-1", "/test/file.txt", LockType::Write)
1042            .await
1043            .unwrap();
1044
1045        // Agent 2 should timeout
1046        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        // Agent 1 holds lock on file1
1064        let _guard1 = manager
1065            .acquire_lock("agent-1", "/test/file1.txt", LockType::Write)
1066            .await
1067            .unwrap();
1068
1069        // Agent 2 holds lock on file2
1070        let _guard2 = manager
1071            .acquire_lock("agent-2", "/test/file2.txt", LockType::Write)
1072            .await
1073            .unwrap();
1074
1075        // Simulate agent 1 waiting for file2
1076        manager
1077            .start_waiting("agent-1", std::path::Path::new("/test/file2.txt"))
1078            .await;
1079
1080        // Agent 2 trying to acquire file1 would create a deadlock
1081        assert!(
1082            manager
1083                .would_deadlock("agent-2", std::path::Path::new("/test/file1.txt"))
1084                .await
1085        );
1086
1087        // But agent 3 trying to acquire file1 would NOT create a deadlock
1088        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        // Record some waiting agents
1100        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        // Clear agent 1's waiting
1116        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}