Skip to main content

prikk_store/
lock.rs

1//! Simple file locks for active-session and ref writers.
2
3use std::path::{Path, PathBuf};
4
5use prikk_error::{PrikkError, Result};
6
7use crate::fsutil::{MutationRoot, create_new_file_required, remove_file_cleanup_best_effort};
8use crate::layout::{LockableContainer, RepositoryLayout};
9
10/// Active session lock acquired before mutating an active WAL tail.
11#[derive(Debug)]
12pub struct ActiveLock {
13    path: PathBuf,
14    relative: PathBuf,
15    mutation_root: MutationRoot,
16}
17
18impl ActiveLock {
19    /// Acquire a lock through exclusive file creation.
20    pub fn acquire(layout: &RepositoryLayout) -> Result<Self> {
21        let path = layout.default_active_lock_path();
22        let relative = layout.repository_relative(&path)?;
23        let mutation_root = layout.repository_mutation_root().clone();
24        acquire_lock_file(&mutation_root, &relative, &path, "active")?;
25        Ok(Self {
26            path,
27            relative,
28            mutation_root,
29        })
30    }
31
32    /// Return lock file path.
33    #[must_use]
34    pub fn path(&self) -> &Path {
35        &self.path
36    }
37
38    pub(crate) fn require_layout(&self, layout: &RepositoryLayout) -> Result<()> {
39        if self
40            .mutation_root
41            .same_authority(layout.repository_mutation_root())
42        {
43            return Ok(());
44        }
45        Err(PrikkError::LockConflict(
46            "active lock belongs to a different repository authority".to_string(),
47        ))
48    }
49}
50
51impl Drop for ActiveLock {
52    fn drop(&mut self) {
53        remove_file_cleanup_best_effort(&self.mutation_root, &self.relative);
54    }
55}
56
57/// Ref-specific lock acquired before publishing one ref pointer.
58#[derive(Debug)]
59pub struct RefLock {
60    path: PathBuf,
61    relative: PathBuf,
62    mutation_root: MutationRoot,
63}
64
65impl RefLock {
66    /// Acquire a ref lock through exclusive file creation.
67    pub fn acquire(layout: &RepositoryLayout, ref_name: &str) -> Result<Self> {
68        let path = layout.ref_lock_path(ref_name);
69        let relative = layout.repository_relative(&path)?;
70        let mutation_root = layout.repository_mutation_root().clone();
71        acquire_lock_file(&mutation_root, &relative, &path, "ref")?;
72        Ok(Self {
73            path,
74            relative,
75            mutation_root,
76        })
77    }
78
79    /// Return lock file path.
80    #[must_use]
81    pub fn path(&self) -> &Path {
82        &self.path
83    }
84}
85
86impl Drop for RefLock {
87    fn drop(&mut self) {
88        remove_file_cleanup_best_effort(&self.mutation_root, &self.relative);
89    }
90}
91
92/// One held lock on a `LockableContainer`. Never constructed directly outside
93/// `acquire_container_locks` -- the sorted-order guarantee that helper provides is only real if
94/// nothing can acquire a container lock any other way.
95#[derive(Debug)]
96struct ContainerLockHandle {
97    relative: PathBuf,
98    mutation_root: MutationRoot,
99}
100
101impl Drop for ContainerLockHandle {
102    fn drop(&mut self) {
103        remove_file_cleanup_best_effort(&self.mutation_root, &self.relative);
104    }
105}
106
107/// RAII guard for one or more container locks, acquired together by `acquire_container_locks` and
108/// released when dropped. Held for its `Drop` effect, not read from -- the same shape
109/// `ActiveLock`/`RefLock` already use.
110#[derive(Debug)]
111pub struct ContainerLockGuard {
112    _handles: Vec<ContainerLockHandle>,
113}
114
115/// Acquire every lock in `containers`, sorted into `LockableContainer`'s fixed `Ord` before any file
116/// is created (design-v1.md ยง15.7's deadlock ruling: a single acquisition helper that sorts the
117/// caller's requested set, not per-call-site ordering discipline) -- so two call sites that each
118/// request `{RefPointerIndex, RefLog}` always acquire them in the same order regardless of which order
119/// their own arguments list them in.
120///
121/// If any acquisition in the sorted sequence fails -- most commonly `LockConflict`, a concurrent
122/// writer or the compactor already holding a later container in the order -- every lock already
123/// acquired during this call is released before the error returns: `handles` (built incrementally,
124/// `?`-propagating on failure) simply drops here, and each already-acquired `ContainerLockHandle`'s
125/// own `Drop` releases it. A partial, leaked hold on early-failure is exactly the wedge shape this
126/// stage's stale-lock recovery work exists to stop introducing more of, so this path must never leave
127/// one behind.
128pub fn acquire_container_locks(
129    layout: &RepositoryLayout,
130    containers: &[LockableContainer],
131) -> Result<ContainerLockGuard> {
132    let mut sorted = containers.to_vec();
133    sorted.sort_unstable();
134    sorted.dedup();
135    let mutation_root = layout.repository_mutation_root().clone();
136    let mut handles = Vec::with_capacity(sorted.len());
137    for container in sorted {
138        let path = layout.lockable_container_lock_path(container);
139        let relative = layout.repository_relative(&path)?;
140        acquire_lock_file(
141            &mutation_root,
142            &relative,
143            &path,
144            container_lock_kind(container),
145        )?;
146        handles.push(ContainerLockHandle {
147            relative,
148            mutation_root: mutation_root.clone(),
149        });
150    }
151    Ok(ContainerLockGuard { _handles: handles })
152}
153
154fn container_lock_kind(container: LockableContainer) -> &'static str {
155    match container {
156        LockableContainer::RefPointerIndex => "container:ref-pointer-index",
157        LockableContainer::RefLog => "container:ref-log",
158        LockableContainer::ReceivedIndex => "container:received-index",
159        LockableContainer::TrustPolicy => "container:trust-policy",
160    }
161}
162
163fn acquire_lock_file(
164    mutation_root: &MutationRoot,
165    relative: &Path,
166    path: &Path,
167    kind: &str,
168) -> Result<()> {
169    let body = lock_body(kind);
170    match create_new_file_required(mutation_root, relative, body.as_bytes()) {
171        Ok(()) => Ok(()),
172        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => Err(
173            PrikkError::LockConflict(format!("{kind} lock already exists: {}", path.display())),
174        ),
175        Err(err) => Err(err.into()),
176    }
177}
178
179fn lock_body(kind: &str) -> String {
180    format!(
181        "pid={}\nkind={kind}\nnote=PR-007 lock has no stale-lock stealing yet\n",
182        std::process::id()
183    )
184}
185
186// DC-71: every test here sets up its scenario via real repository mutation (RepositoryLayout::init
187// or equivalent), which is Linux-only; the module never compiles a non-Linux-meaningful test.
188#[cfg(all(test, target_os = "linux"))]
189mod tests;