Skip to main content

aaai_core/config/
lock.rs

1//! Definition file write locking — prevents concurrent saves.
2//!
3//! A lock file `<definition>.lock` is created before writing and removed
4//! afterwards.  If the lock file already exists and is recent (< 60 s old),
5//! the save is aborted with an error.
6
7use std::path::Path;
8use std::time::{Duration, SystemTime};
9
10const LOCK_TTL: Duration = Duration::from_secs(60);
11const LOCK_EXT: &str = "lock";
12
13/// Acquire a write lock for `definition_path`.
14///
15/// Returns `Err` when the lock is already held by another process.
16/// On success, returns a [`LockGuard`] that releases the lock on drop.
17pub fn acquire(definition_path: &Path) -> anyhow::Result<LockGuard> {
18    let lock_path = definition_path.with_extension(LOCK_EXT);
19
20    if lock_path.exists() {
21        // Check whether the lock is stale.
22        if let Ok(meta) = std::fs::metadata(&lock_path) {
23            if let Ok(modified) = meta.modified() {
24                if SystemTime::now().duration_since(modified).unwrap_or_default() < LOCK_TTL {
25                    anyhow::bail!(
26                        "Definition file is locked by another process: {}.\n\
27                         Delete {} to force-unlock.",
28                        definition_path.display(),
29                        lock_path.display()
30                    );
31                }
32                // Stale lock — remove it.
33                log::warn!("Removing stale lock: {}", lock_path.display());
34                let _ = std::fs::remove_file(&lock_path);
35            }
36        }
37    }
38
39    std::fs::write(&lock_path, format!("pid:{}", std::process::id()))?;
40    log::debug!("Lock acquired: {}", lock_path.display());
41    Ok(LockGuard { lock_path })
42}
43
44/// RAII guard that releases the lock on drop.
45#[must_use]
46pub struct LockGuard {
47    lock_path: std::path::PathBuf,
48}
49
50impl Drop for LockGuard {
51    fn drop(&mut self) {
52        if let Err(e) = std::fs::remove_file(&self.lock_path) {
53            log::warn!("Could not remove lock file {}: {e}", self.lock_path.display());
54        } else {
55            log::debug!("Lock released: {}", self.lock_path.display());
56        }
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use std::path::PathBuf;
64
65    #[test]
66    fn lock_acquire_and_release() {
67        let tmp = tempfile::tempdir().unwrap();
68        let def = tmp.path().join("audit.yaml");
69        std::fs::write(&def, "").unwrap();
70
71        {
72            let _guard = acquire(&def).unwrap();
73            let lock = def.with_extension("lock");
74            assert!(lock.exists(), "lock file should exist while guard is alive");
75        }
76
77        let lock = def.with_extension("lock");
78        assert!(!lock.exists(), "lock file should be removed on drop");
79    }
80
81    #[test]
82    fn double_lock_fails() {
83        let tmp = tempfile::tempdir().unwrap();
84        let def = tmp.path().join("audit.yaml");
85        std::fs::write(&def, "").unwrap();
86
87        let _guard1 = acquire(&def).unwrap();
88        let result2 = acquire(&def);
89        assert!(result2.is_err(), "second acquire should fail while lock is held");
90    }
91}