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
64    #[test]
65    fn lock_acquire_and_release() {
66        let tmp = tempfile::tempdir().unwrap();
67        let def = tmp.path().join("audit.yaml");
68        std::fs::write(&def, "").unwrap();
69
70        {
71            let _guard = acquire(&def).unwrap();
72            let lock = def.with_extension("lock");
73            assert!(lock.exists(), "lock file should exist while guard is alive");
74        }
75
76        let lock = def.with_extension("lock");
77        assert!(!lock.exists(), "lock file should be removed on drop");
78    }
79
80    #[test]
81    fn double_lock_fails() {
82        let tmp = tempfile::tempdir().unwrap();
83        let def = tmp.path().join("audit.yaml");
84        std::fs::write(&def, "").unwrap();
85
86        let _guard1 = acquire(&def).unwrap();
87        let result2 = acquire(&def);
88        assert!(result2.is_err(), "second acquire should fail while lock is held");
89    }
90}