spacetimedb_fs_utils/
lockfile.rs

1use crate::create_parent_dir;
2use std::path::{Path, PathBuf};
3
4#[derive(thiserror::Error, Debug)]
5pub enum LockfileError {
6    #[error("Failed to acquire lock on {file_path:?}: failed to create lockfile {lock_path:?}: {cause}")]
7    Acquire {
8        file_path: PathBuf,
9        lock_path: PathBuf,
10        #[source]
11        cause: std::io::Error,
12    },
13    #[error("Failed to release lock: failed to delete lockfile {lock_path:?}: {cause}")]
14    Release {
15        lock_path: PathBuf,
16        #[source]
17        cause: std::io::Error,
18    },
19}
20
21#[derive(Debug)]
22/// A file used as an exclusive lock on access to another file.
23///
24/// Constructing a `Lockfile` creates the `path` with [`std::fs::File::create_new`],
25/// a.k.a. `O_EXCL`, erroring if the file already exists.
26///
27/// Dropping a `Lockfile` deletes the `path`, releasing the lock.
28///
29/// Used to guarantee exclusive access to the system config file,
30/// in order to prevent racy concurrent modifications.
31pub struct Lockfile {
32    path: PathBuf,
33}
34
35impl Lockfile {
36    /// Acquire an exclusive lock on the file `file_path`.
37    ///
38    /// `file_path` should be the full path of the file to which to acquire exclusive access.
39    pub fn for_file<P: AsRef<Path>>(file_path: P) -> Result<Self, LockfileError> {
40        let file_path = file_path.as_ref();
41        // TODO: Someday, it would be nice to use OS locks to minimize edge cases (see
42        // https://github.com/clockworklabs/SpacetimeDB/pull/1341#issuecomment-2151018992).
43        //
44        // Currently, our files can be left around if a process is unceremoniously killed (most
45        // commonly with Ctrl-C, but this would also apply to e.g. power failure).
46        // See https://github.com/clockworklabs/SpacetimeDB/issues/1339.
47        let path = Self::lock_path(file_path);
48
49        let fail = |cause| LockfileError::Acquire {
50            lock_path: path.clone(),
51            file_path: file_path.to_path_buf(),
52            cause,
53        };
54        // Ensure the directory exists before attempting to create the lockfile.
55        create_parent_dir(file_path).map_err(fail)?;
56        // Open with `create_new`, which fails if the file already exists.
57        std::fs::File::create_new(&path).map_err(fail)?;
58        Ok(Lockfile { path })
59    }
60
61    /// Returns the path of a lockfile for the file `file_path`,
62    /// without actually acquiring the lock.
63    pub fn lock_path<P: AsRef<Path>>(file_path: P) -> PathBuf {
64        file_path.as_ref().with_extension("lock")
65    }
66
67    fn release_internal(path: &Path) -> Result<(), LockfileError> {
68        std::fs::remove_file(path).map_err(|cause| LockfileError::Release {
69            lock_path: path.to_path_buf(),
70            cause,
71        })
72    }
73
74    /// Release the lock, with the opportunity to handle the error from failing to delete the lockfile.
75    ///
76    /// Dropping a [`Lockfile`] will release the lock, but will panic on failure rather than returning `Err`.
77    pub fn release(self) -> Result<(), LockfileError> {
78        // Don't run the destructor, which does the same thing, but panics on failure.
79        let mut this = std::mem::ManuallyDrop::new(self);
80        let path = std::mem::take(&mut this.path);
81        let res = Self::release_internal(&path);
82        drop(path);
83        res
84    }
85}
86
87impl Drop for Lockfile {
88    fn drop(&mut self) {
89        Self::release_internal(&self.path).unwrap();
90    }
91}