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                )));
96            }
97        }
98        Err(error) => {
99            return Err(anyhow!(error))
100                .with_context(|| format!("create lock dir {}", lock_dir.display()));
101        }
102    }
103
104    let effective_label = if trimmed_label.is_empty() {
105        "unspecified"
106    } else {
107        trimmed_label
108    };
109    let owner = LockOwner {
110        pid: std::process::id(),
111        started_at: timeutil::now_utc_rfc3339()?,
112        command: command_line(),
113        label: effective_label.to_string(),
114    };
115
116    let owner_path = if is_task_label && lock_dir.exists() {
117        let counter = TASK_OWNER_COUNTER.fetch_add(1, Ordering::SeqCst);
118        lock_dir.join(format!(
119            "{}{}_{}",
120            TASK_OWNER_PREFIX,
121            std::process::id(),
122            counter
123        ))
124    } else {
125        lock_dir.join(OWNER_FILE_NAME)
126    };
127
128    if let Err(error) = write_lock_owner(&owner_path, &owner) {
129        if let Err(remove_error) = fs::remove_file(&owner_path) {
130            log::debug!(
131                "Failed to remove owner file {}: {}",
132                owner_path.display(),
133                remove_error
134            );
135        }
136        if let Err(remove_error) = fs::remove_dir(lock_dir) {
137            log::debug!(
138                "Failed to remove lock directory {}: {}",
139                lock_dir.display(),
140                remove_error
141            );
142        }
143        return Err(error);
144    }
145
146    Ok(DirLock {
147        lock_dir: lock_dir.to_path_buf(),
148        owner_path,
149    })
150}