Skip to main content

ralph/lock/
acquisition.rs

1//! Lock acquisition and shared-lock semantics.
2//!
3//! Responsibilities:
4//! - Create lock directories and owner files.
5//! - Apply stale-lock force-removal and shared supervisor/task lock rules.
6//! - Detect supervising-process ownership for callers that should avoid re-locking.
7//!
8//! Not handled here:
9//! - PID liveness implementation details.
10//! - Lock cleanup retries after drop.
11//!
12//! Invariants/assumptions:
13//! - A `task` lock may coexist only with a supervising `owner` file.
14//! - Task owner sidecars must be unique per acquisition attempt.
15
16use super::{
17    DirLock,
18    owner::{
19        LockOwner, OWNER_FILE_NAME, TASK_OWNER_PREFIX, command_line, is_supervising_label,
20        parse_lock_owner, read_lock_owner, write_lock_owner,
21    },
22    stale::{format_lock_error, inspect_existing_lock},
23};
24use crate::timeutil;
25use anyhow::{Context, Result, anyhow};
26use std::fs;
27use std::path::{Path, PathBuf};
28use std::sync::atomic::{AtomicUsize, Ordering};
29
30static TASK_OWNER_COUNTER: AtomicUsize = AtomicUsize::new(0);
31
32pub fn queue_lock_dir(repo_root: &Path) -> PathBuf {
33    repo_root.join(".ralph").join("lock")
34}
35
36pub fn is_supervising_process(lock_dir: &Path) -> Result<bool> {
37    let owner_path = lock_dir.join(OWNER_FILE_NAME);
38    let raw = match fs::read_to_string(&owner_path) {
39        Ok(raw) => raw,
40        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
41        Err(err) => {
42            return Err(anyhow!(err))
43                .with_context(|| format!("read lock owner {}", owner_path.display()));
44        }
45    };
46
47    let owner = match parse_lock_owner(&raw) {
48        Some(owner) => owner,
49        None => return Ok(false),
50    };
51    Ok(is_supervising_label(&owner.label))
52}
53
54pub fn acquire_dir_lock(lock_dir: &Path, label: &str, force: bool) -> Result<DirLock> {
55    log::debug!(
56        "acquiring dir lock: {} (label: {})",
57        lock_dir.display(),
58        label
59    );
60    if let Some(parent) = lock_dir.parent() {
61        fs::create_dir_all(parent)
62            .with_context(|| format!("create lock parent {}", parent.display()))?;
63    }
64
65    let trimmed_label = label.trim();
66    let is_task_label = trimmed_label == "task";
67
68    match fs::create_dir(lock_dir) {
69        Ok(()) => {}
70        Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
71            let existing = inspect_existing_lock(lock_dir, read_lock_owner);
72
73            if force && existing.is_stale {
74                if let Err(remove_error) = fs::remove_dir_all(lock_dir) {
75                    log::debug!(
76                        "Failed to remove stale lock directory {}: {}",
77                        lock_dir.display(),
78                        remove_error
79                    );
80                }
81                return acquire_dir_lock(lock_dir, label, false);
82            }
83
84            if !(is_task_label
85                && existing
86                    .owner
87                    .as_ref()
88                    .is_some_and(|owner| is_supervising_label(&owner.label)))
89            {
90                return Err(anyhow!(format_lock_error(
91                    lock_dir,
92                    existing.owner.as_ref(),
93                    existing.is_stale,
94                    existing.owner_unreadable,
95                    existing.staleness,
96                )));
97            }
98        }
99        Err(error) => {
100            return Err(anyhow!(error))
101                .with_context(|| format!("create lock dir {}", lock_dir.display()));
102        }
103    }
104
105    let effective_label = if trimmed_label.is_empty() {
106        "unspecified"
107    } else {
108        trimmed_label
109    };
110    let owner = LockOwner {
111        pid: std::process::id(),
112        started_at: timeutil::now_utc_rfc3339()?,
113        command: command_line(),
114        label: effective_label.to_string(),
115    };
116
117    let owner_path = if is_task_label && lock_dir.exists() {
118        let counter = TASK_OWNER_COUNTER.fetch_add(1, Ordering::SeqCst);
119        lock_dir.join(format!(
120            "{}{}_{}",
121            TASK_OWNER_PREFIX,
122            std::process::id(),
123            counter
124        ))
125    } else {
126        lock_dir.join(OWNER_FILE_NAME)
127    };
128
129    if let Err(error) = write_lock_owner(&owner_path, &owner) {
130        if let Err(remove_error) = fs::remove_file(&owner_path) {
131            log::debug!(
132                "Failed to remove owner file {}: {}",
133                owner_path.display(),
134                remove_error
135            );
136        }
137        if let Err(remove_error) = fs::remove_dir(lock_dir) {
138            log::debug!(
139                "Failed to remove lock directory {}: {}",
140                lock_dir.display(),
141                remove_error
142            );
143        }
144        return Err(error);
145    }
146
147    Ok(DirLock {
148        lock_dir: lock_dir.to_path_buf(),
149        owner_path,
150    })
151}