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    journal: PathBuf,
49    #[cfg(unix)]
50    file: fs::File,
51}
52
53impl JournalLock {
54    /// Return the canonical journal location protected by this real sidecar guard.
55    #[must_use]
56    pub fn journal(&self) -> &Path {
57        &self.journal
58    }
59
60    /// Acquire the journal sidecar without waiting or following its symlink.
61    ///
62    /// # Errors
63    /// Returns typed lock contention, unsafe-entry or filesystem failure.
64    pub fn acquire(journal_path: &Path) -> Result<Self, JournalLockError> {
65        let name = journal_path
66            .file_name()
67            .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
68        let parent = journal_path
69            .parent()
70            .filter(|parent| !parent.as_os_str().is_empty())
71            .unwrap_or_else(|| Path::new("."));
72        // Sidecars also serialize directory-shaped artifact publication locations.
73        let journal = parent.canonicalize()?.join(name);
74        let path = journal_lock_path(&journal);
75
76        #[cfg(unix)]
77        {
78            acquire_supported(&path, journal)
79        }
80
81        #[cfg(not(unix))]
82        {
83            Err(io::Error::new(
84                io::ErrorKind::Unsupported,
85                format!(
86                    "journal locking is unsupported on this host: {}",
87                    path.display()
88                ),
89            )
90            .into())
91        }
92    }
93}
94
95#[cfg(unix)]
96impl Drop for JournalLock {
97    fn drop(&mut self) {
98        file_lock::unlock(&self.file);
99    }
100}
101
102#[cfg(unix)]
103fn acquire_supported(path: &Path, journal: PathBuf) -> Result<JournalLock, JournalLockError> {
104    match file_lock::acquire(path) {
105        Ok(file) => Ok(JournalLock { journal, file }),
106        Err(FileLockError::Locked) => Err(JournalLockError::Locked {
107            lock_path: path.to_string_lossy().to_string(),
108        }),
109        Err(FileLockError::UnsafeEntry { kind }) => Err(JournalLockError::UnsafeEntry {
110            lock_path: path.to_string_lossy().to_string(),
111            kind,
112        }),
113        Err(FileLockError::Io(error)) => Err(JournalLockError::Io(error)),
114    }
115}
116
117fn journal_lock_path(path: &Path) -> PathBuf {
118    let mut lock_path = path.as_os_str().to_os_string();
119    lock_path.push(".lock");
120    PathBuf::from(lock_path)
121}
122
123#[cfg(test)]
124mod tests;