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}