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 fs4::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        <File as FileExt>::lock(&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        <File as 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        <File as FileExt>::try_lock(&file)
44            .map_err(|e| io::Error::other(format!("lock not acquired: {e}")))?;
45        Ok(Self { file })
46    }
47}
48
49impl Drop for StoreLock {
50    fn drop(&mut self) {
51        let _ = <File as FileExt>::unlock(&self.file);
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use tempfile::TempDir;
59
60    #[test]
61    fn exclusive_then_shared_release_roundtrip() {
62        let dir = TempDir::new().unwrap();
63        {
64            let _g = StoreLock::exclusive(dir.path()).unwrap();
65            assert!(dir.path().join(".lock").is_file());
66        } // released on drop
67        let _g = StoreLock::shared(dir.path()).unwrap();
68    }
69
70    #[test]
71    fn try_exclusive_fails_while_held() {
72        let dir = TempDir::new().unwrap();
73        let _held = StoreLock::exclusive(dir.path()).unwrap();
74        assert!(StoreLock::try_exclusive(dir.path()).is_err());
75    }
76}