Skip to main content

git_policy_cli/
live_lock.rs

1//! What:
2//!  Wait for a lock another live process holds, with a visible notice and a generous backstop.
3//! Why:
4//!  A holder that is alive but stuck (a hung hook, a stalled tool) would otherwise stall every later command
5//!  with no output at all.
6//!  After a short silence the waiter says whom it waits for and when it will give up,
7//!  and past the backstop it fails closed: the command stops with the holder named and the lock left in place.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! await acquireOwnerLock({ lockDirectory, signal: AbortSignal.timeout(limitMs), onSlow: announce });
12//! ```
13
14/// Positive-seconds parsing shared with the push deadline.
15use super::bounded_push::positive_seconds;
16/// The owner lock, its bounded acquire and the notice interface.
17use super::owner_lock::{
18    DEFAULT_POLL_DELAY, OwnerLock, OwnerLockError, WaitNotice, acquire_owner_lock_for,
19};
20/// The lock record whose owner the notice names.
21use super::owner_lock_record::{PublishedOwner, read_owner_lock_record};
22/// A commit's place in line, and how it reads.
23use super::wait_progress::{QueuePlace, queue_place_text};
24/// Borrowed OS text of the environment value.
25use std::ffi::OsStr;
26/// `Write` is the sink the notice goes to.
27use std::io::Write;
28/// `Path` and `PathBuf` are borrowed and owned filesystem paths.
29use std::path::{Path, PathBuf};
30/// Time spans.
31use std::time::Duration;
32
33/// Environment variable that sets, in whole seconds, how long a command waits for a lock held by a live process.
34pub const LIVE_WAIT_VARIABLE: &str = "CLI_GIT_LOCK_WAIT_SECONDS";
35
36/// How long a command waits for a live holder when the variable is absent or unusable:
37///  longer than any ordinary hook run, short enough that a stuck holder is noticed within the hour.
38pub const DEFAULT_LIVE_WAIT: Duration = Duration::from_secs(30 * 60);
39
40/// What:
41///  The wait limit for a value of the variable.
42/// Why:
43///   A missing, zero or malformed value falls back to the default,
44///  never to "no limit".
45///
46/// In TS you'd write (pseudocode):
47/// ```ts
48/// positiveSeconds(value) ?? DEFAULT_LIVE_WAIT_MS
49/// ```
50pub fn live_wait_limit_from(value: Option<&OsStr>) -> Duration {
51    let Some(text) = value else {
52        return DEFAULT_LIVE_WAIT;
53    };
54    return positive_seconds(LIVE_WAIT_VARIABLE, text).unwrap_or(DEFAULT_LIVE_WAIT);
55}
56
57/// The wait limit this process runs under: the variable in its environment, or the default.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// liveWaitLimitMs(process.env.CLI_GIT_LOCK_WAIT_SECONDS)
62/// ```
63pub fn live_wait_limit() -> Duration {
64    return live_wait_limit_from(std::env::var_os(LIVE_WAIT_VARIABLE).as_deref());
65}
66
67/// What:
68///  What one notice of a wait for a lock says.
69/// Why:
70///   Every wait for a lock names the same facts:
71///  what it waits for, who holds it, how long it has waited, where it gives up, and its place when commits queue.
72///
73/// In TS you'd write (pseudocode):
74/// ```ts
75/// type LockWaitFacts = { subject; lockDirectory; holderPid?; waitedMs; limitMs; place?; repeat };
76/// ```
77#[derive(Clone, Copy, Debug)]
78pub struct LockWaitFacts<'facts> {
79    /// What the lock protects, in words.
80    pub subject: &'facts str,
81    /// The lock being waited for.
82    pub lock_directory: &'facts Path,
83    /// The live holder's process, when its record can be read.
84    pub holder_pid: Option<i64>,
85    /// How long the wait has lasted.
86    pub waited: Duration,
87    /// How long the wait lasts before it gives up.
88    pub limit: Duration,
89    /// This commit's place among the commits that queue for the same thing, when the registry knows it.
90    pub place: Option<QueuePlace>,
91    /// Whether an earlier notice of this wait already printed the hint.
92    pub repeat: bool,
93}
94
95/// What:
96///  The notice a slow wait prints.
97/// Why:
98///   It names the holder, the lock and both durations so the person can look at the holder or raise the limit;
99///  the first notice of a wait adds what to do, and the repeats stay short.
100///
101/// In TS you'd write (pseudocode):
102/// ```ts
103/// `cli-git: still waiting for ${holder} to release ${subject} ...`
104/// ```
105pub fn slow_wait_line(facts: &LockWaitFacts<'_>) -> String {
106    let holder: String = match facts.holder_pid {
107        Some(pid) => format!("process {pid}"),
108        None => String::from("another cli-git process"),
109    };
110    let place: String = match facts.place {
111        Some(place) => format!(" {}", queue_place_text(place)),
112        None => String::new(),
113    };
114    let hint: &str = if facts.repeat {
115        ""
116    } else {
117        " Hint: if you started another Git command in a second terminal or session, let it finish and run this one \
118         again; if no such command is running, check with `ps` and report the stuck process."
119    };
120    return format!(
121        "cli-git: still waiting for {holder} to release {} ({}) after {} seconds; \
122         giving up after {} seconds ({LIVE_WAIT_VARIABLE} changes this), leaving the lock in place.{place}{hint}",
123        facts.subject,
124        facts.lock_directory.display(),
125        facts.waited.as_secs(),
126        facts.limit.as_secs()
127    );
128}
129
130/// What:
131///  A wait notice that prints a line to a sink once the wait is slow, and again at every interval.
132/// Why:
133///   Brief contention stays silent, as the incumbent's waits were;
134///  a long wait stops looking like a hang, and a wait that is still going keeps saying so.
135///
136/// In TS you'd write (pseudocode):
137/// ```ts
138/// const onSlow = (waitedMs) => sink.write(slowWaitLine(...))
139/// ```
140pub struct HolderNotice<Sink: Write> {
141    /// The lock being waited for.
142    lock_directory: PathBuf,
143    /// What the lock protects, in words.
144    subject: &'static str,
145    /// The limit the wait gives up at.
146    limit: Duration,
147    /// Where the line goes.
148    sink: Sink,
149    /// How many lines this wait has printed.
150    printed: u32,
151}
152
153impl<Sink: Write> HolderNotice<Sink> {
154    /// A notice for one lock, subject and limit.
155    pub fn new(lock_directory: &Path, subject: &'static str, limit: Duration, sink: Sink) -> Self {
156        return Self {
157            lock_directory: lock_directory.to_path_buf(),
158            subject,
159            limit,
160            sink,
161            printed: 0,
162        };
163    }
164}
165
166impl<Sink: Write> WaitNotice for HolderNotice<Sink> {
167    /// The first busy attempt says nothing: most waits end within the grace.
168    fn waiting(&mut self) {}
169
170    /// Print the wait line naming the holder when its record can be read;
171    ///  the wait calls this after the grace and at every interval.
172    fn slow(&mut self, waited: Duration) {
173        let holder_pid: Option<i64> = match read_owner_lock_record(&self.lock_directory) {
174            Ok(PublishedOwner::Owner(record)) => Some(record.owner_pid),
175            Ok(PublishedOwner::Busy) | Err(_) => None,
176        };
177        let line: String = slow_wait_line(&LockWaitFacts {
178            subject: self.subject,
179            lock_directory: &self.lock_directory,
180            holder_pid,
181            waited,
182            limit: self.limit,
183            place: None,
184            repeat: self.printed > 0,
185        });
186        self.printed += 1;
187        // Terminal output error: the notice is advisory; a failed write must not stop the wait.
188        let _written = writeln!(self.sink, "{line}");
189    }
190}
191
192/// What:
193///  Take an owner lock, waiting for a live holder with a slow-wait notice on standard error,
194///  and giving up at this process's wait limit.
195/// Why:
196///   One call replaces the silent unbounded wait at every lock a command may queue behind.
197///
198/// In TS you'd write (pseudocode):
199/// ```ts
200/// await acquireLiveOwnerLock({ lockDirectory, subject });
201/// ```
202pub fn acquire_live_owner_lock(
203    lock_directory: &Path,
204    subject: &'static str,
205) -> Result<OwnerLock, OwnerLockError> {
206    let limit: Duration = live_wait_limit();
207    let mut notice: HolderNotice<std::io::Stderr> =
208        HolderNotice::new(lock_directory, subject, limit, std::io::stderr());
209    return acquire_owner_lock_for(lock_directory, DEFAULT_POLL_DELAY, &mut notice, limit);
210}
211
212/// Wait notices and limits are controlled in the test module.
213#[cfg(test)]
214#[path = "live_lock_tests.rs"]
215mod tests;