Skip to main content

act_store/
lock.rs

1//! Advisory file lock over `<store>/.lock`, guarding `index.json` mutations.
2
3use std::fs::{File, OpenOptions};
4use std::io;
5use std::path::Path;
6
7use fs2::FileExt;
8
9/// RAII advisory lock. The lock is released when dropped.
10#[derive(Debug)]
11pub struct StoreLock {
12    file: File,
13}
14
15fn open_lockfile(root: &Path) -> io::Result<File> {
16    std::fs::create_dir_all(root)?;
17    OpenOptions::new()
18        .create(true)
19        .truncate(false)
20        .read(true)
21        .write(true)
22        .open(root.join(".lock"))
23}
24
25impl StoreLock {
26    /// Block until an exclusive (writer) lock is held.
27    pub fn exclusive(root: &Path) -> io::Result<Self> {
28        let file = open_lockfile(root)?;
29        FileExt::lock_exclusive(&file)?;
30        Ok(Self { file })
31    }
32
33    /// Block until a shared (reader) lock is held.
34    pub fn shared(root: &Path) -> io::Result<Self> {
35        let file = open_lockfile(root)?;
36        FileExt::lock_shared(&file)?;
37        Ok(Self { file })
38    }
39
40    /// Non-blocking exclusive lock; errors if it cannot be acquired now.
41    pub fn try_exclusive(root: &Path) -> io::Result<Self> {
42        let file = open_lockfile(root)?;
43        FileExt::try_lock_exclusive(&file)?;
44        Ok(Self { file })
45    }
46}
47
48impl Drop for StoreLock {
49    fn drop(&mut self) {
50        let _ = FileExt::unlock(&self.file);
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use tempfile::TempDir;
58
59    #[test]
60    fn exclusive_then_shared_release_roundtrip() {
61        let dir = TempDir::new().unwrap();
62        {
63            let _g = StoreLock::exclusive(dir.path()).unwrap();
64            assert!(dir.path().join(".lock").is_file());
65        } // released on drop
66        let _g = StoreLock::shared(dir.path()).unwrap();
67    }
68
69    #[test]
70    fn try_exclusive_fails_while_held() {
71        let dir = TempDir::new().unwrap();
72        let _held = StoreLock::exclusive(dir.path()).unwrap();
73        assert!(StoreLock::try_exclusive(dir.path()).is_err());
74    }
75}