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 thiserror::Error as ThisError;
9
10///
11/// `JournalLockError`
12///
13/// Typed failure to acquire exclusive access to a journal's regular sidecar file.
14///
15
16#[derive(Debug, ThisError)]
17pub enum JournalLockError {
18    /// Another owner holds the sidecar lock.
19    #[error("filesystem authority is locked: {lock_path}")]
20    Locked {
21        /// Sidecar path.
22        lock_path: String,
23    },
24
25    /// The sidecar was replaced by a non-regular entry.
26    #[error("unsafe lock entry at {lock_path}: {kind}")]
27    UnsafeEntry {
28        /// Sidecar path.
29        lock_path: String,
30        /// Observed entry kind.
31        kind: String,
32    },
33
34    /// Filesystem IO failed.
35    #[error(transparent)]
36    Io(#[from] io::Error),
37}
38
39///
40/// `JournalLock`
41///
42/// Exclusive journal mutation guard. Dropping it releases the host file lock.
43///
44
45#[derive(Debug)]
46pub struct JournalLock {
47    journal: PathBuf,
48    #[cfg(unix)]
49    file: fs::File,
50}
51
52impl JournalLock {
53    /// Return the canonical journal location protected by this real sidecar guard.
54    #[must_use]
55    pub fn journal(&self) -> &Path {
56        &self.journal
57    }
58
59    /// Acquire the journal sidecar without waiting or following its symlink.
60    ///
61    /// # Errors
62    /// Returns typed lock contention, unsafe-entry or filesystem failure.
63    pub fn acquire(journal_path: &Path) -> Result<Self, JournalLockError> {
64        let name = journal_path
65            .file_name()
66            .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput))?;
67        let parent = journal_path
68            .parent()
69            .filter(|parent| !parent.as_os_str().is_empty())
70            .unwrap_or_else(|| Path::new("."));
71        // Sidecars also serialize directory-shaped artifact publication locations.
72        let journal = parent.canonicalize()?.join(name);
73        let path = journal_lock_path(&journal);
74
75        #[cfg(unix)]
76        {
77            acquire_supported(&path, journal)
78        }
79
80        #[cfg(not(unix))]
81        {
82            Err(io::Error::new(
83                io::ErrorKind::Unsupported,
84                format!(
85                    "journal locking is unsupported on this host: {}",
86                    path.display()
87                ),
88            )
89            .into())
90        }
91    }
92}
93
94#[cfg(unix)]
95impl Drop for JournalLock {
96    fn drop(&mut self) {
97        let _ = ic_host_fs::durable::unlock(&self.file);
98    }
99}
100
101#[cfg(unix)]
102fn acquire_supported(path: &Path, journal: PathBuf) -> Result<JournalLock, JournalLockError> {
103    use ic_host_fs::durable::{
104        RegularFileLockError, open_private_regular_lock_file, try_lock_exclusive,
105    };
106    let file = open_private_regular_lock_file(path).map_err(|error| match error {
107        RegularFileLockError::NotRegular => JournalLockError::UnsafeEntry {
108            lock_path: path.to_string_lossy().to_string(),
109            kind: match fs::symlink_metadata(path) {
110                Ok(metadata) if metadata.file_type().is_symlink() => "Symlink",
111                Ok(metadata) if metadata.is_dir() => "Directory",
112                _ => "Special",
113            }
114            .to_owned(),
115        },
116        error => JournalLockError::Io(error.into()),
117    })?;
118    try_lock_exclusive(&file).map_err(|error| {
119        if error.kind() == io::ErrorKind::WouldBlock {
120            JournalLockError::Locked {
121                lock_path: path.to_string_lossy().to_string(),
122            }
123        } else {
124            JournalLockError::Io(error)
125        }
126    })?;
127    Ok(JournalLock { journal, file })
128}
129
130fn journal_lock_path(path: &Path) -> PathBuf {
131    let mut lock_path = path.as_os_str().to_os_string();
132    lock_path.push(".lock");
133    PathBuf::from(lock_path)
134}
135
136#[cfg(test)]
137mod tests;