Skip to main content

ic_backup/ops/persistence/journal_lock/
mod.rs

1//! Module: `persistence::journal_lock`
2//!
3//! Responsibility: serialize mutation of a persisted journal with a sidecar lock.
4//! Does not own: journal validation, workflow policy, or domain error projection.
5
6use std::{fs, io, path::Path, path::PathBuf};
7
8use super::file_lock::{self, FileLockError};
9use thiserror::Error as ThisError;
10
11///
12/// `JournalLockError`
13///
14/// Typed failure to acquire exclusive access to a journal's regular sidecar file.
15///
16
17#[derive(Debug, ThisError)]
18pub enum JournalLockError {
19    /// Another owner holds the sidecar lock.
20    #[error("filesystem authority is locked: {lock_path}")]
21    Locked {
22        /// Sidecar path.
23        lock_path: String,
24    },
25
26    /// The sidecar was replaced by a non-regular entry.
27    #[error("unsafe lock entry at {lock_path}: {kind}")]
28    UnsafeEntry {
29        /// Sidecar path.
30        lock_path: String,
31        /// Observed entry kind.
32        kind: String,
33    },
34
35    /// Filesystem IO failed.
36    #[error(transparent)]
37    Io(#[from] io::Error),
38}
39
40///
41/// `JournalLock`
42///
43/// Exclusive journal mutation guard. Dropping it releases the host file lock.
44///
45
46#[derive(Debug)]
47pub struct JournalLock {
48    #[cfg(unix)]
49    file: fs::File,
50}
51
52impl JournalLock {
53    /// Acquire the journal sidecar without waiting or following its symlink.
54    ///
55    /// # Errors
56    /// Returns typed lock contention, unsafe-entry or filesystem failure.
57    pub fn acquire(journal_path: &Path) -> Result<Self, JournalLockError> {
58        let path = journal_lock_path(journal_path);
59
60        #[cfg(unix)]
61        {
62            acquire_supported(&path)
63        }
64
65        #[cfg(not(unix))]
66        {
67            Err(io::Error::new(
68                io::ErrorKind::Unsupported,
69                format!(
70                    "journal locking is unsupported on this host: {}",
71                    path.display()
72                ),
73            )
74            .into())
75        }
76    }
77}
78
79#[cfg(unix)]
80impl Drop for JournalLock {
81    fn drop(&mut self) {
82        file_lock::unlock(&self.file);
83    }
84}
85
86#[cfg(unix)]
87fn acquire_supported(path: &Path) -> Result<JournalLock, JournalLockError> {
88    match file_lock::acquire(path) {
89        Ok(file) => Ok(JournalLock { file }),
90        Err(FileLockError::Locked) => Err(JournalLockError::Locked {
91            lock_path: path.to_string_lossy().to_string(),
92        }),
93        Err(FileLockError::UnsafeEntry { kind }) => Err(JournalLockError::UnsafeEntry {
94            lock_path: path.to_string_lossy().to_string(),
95            kind,
96        }),
97        Err(FileLockError::Io(error)) => Err(JournalLockError::Io(error)),
98    }
99}
100
101fn journal_lock_path(path: &Path) -> PathBuf {
102    let mut lock_path = path.as_os_str().to_os_string();
103    lock_path.push(".lock");
104    PathBuf::from(lock_path)
105}
106
107#[cfg(test)]
108mod tests;