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/// Borrowed OS text of the environment value.
23use std::ffi::OsStr;
24/// `Write` is the sink the notice goes to.
25use std::io::Write;
26/// `Path` and `PathBuf` are borrowed and owned filesystem paths.
27use std::path::{Path, PathBuf};
28/// Time spans.
29use std::time::Duration;
30
31/// Environment variable that sets, in whole seconds, how long a command waits for a lock held by a live process.
32pub const LIVE_WAIT_VARIABLE: &str = "CLI_GIT_LOCK_WAIT_SECONDS";
33
34/// How long a command waits for a live holder when the variable is absent or unusable:
35/// longer than any ordinary hook run, short enough that a stuck holder is noticed within the hour.
36pub const DEFAULT_LIVE_WAIT: Duration = Duration::from_secs(30 * 60);
37
38/// What:
39/// The wait limit for a value of the variable.
40/// Why:
41/// A missing, zero or malformed value falls back to the default,
42/// never to "no limit".
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// positiveSeconds(value) ?? DEFAULT_LIVE_WAIT_MS
47/// ```
48pub fn live_wait_limit_from(value: Option<&OsStr>) -> Duration {
49 let Some(text) = value else {
50 return DEFAULT_LIVE_WAIT;
51 };
52 return positive_seconds(LIVE_WAIT_VARIABLE, text).unwrap_or(DEFAULT_LIVE_WAIT);
53}
54
55/// The wait limit this process runs under: the variable in its environment, or the default.
56///
57/// In TS you'd write (pseudocode):
58/// ```ts
59/// liveWaitLimitMs(process.env.CLI_GIT_LOCK_WAIT_SECONDS)
60/// ```
61pub fn live_wait_limit() -> Duration {
62 return live_wait_limit_from(std::env::var_os(LIVE_WAIT_VARIABLE).as_deref());
63}
64
65/// What:
66/// The notice a slow wait prints.
67/// Why:
68/// It names the holder, the lock and both durations so the person can look at the holder or raise the limit.
69///
70/// In TS you'd write (pseudocode):
71/// ```ts
72/// `cli-git: still waiting for ${holder} to release ${subject} ...`
73/// ```
74pub fn slow_wait_line(
75 subject: &str,
76 lock_directory: &Path,
77 holder_pid: Option<i64>,
78 waited: Duration,
79 limit: Duration,
80) -> String {
81 let holder: String = match holder_pid {
82 Some(pid) => format!("process {pid}"),
83 None => String::from("another cli-git process"),
84 };
85 return format!(
86 "cli-git: still waiting for {holder} to release {subject} ({}) after {} seconds; \
87 giving up after {} seconds ({LIVE_WAIT_VARIABLE} changes this), leaving the lock in place. Hint: if you started \
88 another Git command in a second terminal or session, let it finish and run this one again; if no such \
89 command is running, check with `ps` and report the stuck process.",
90 lock_directory.display(),
91 waited.as_secs(),
92 limit.as_secs()
93 );
94}
95
96/// What:
97/// A wait notice that prints one line to a sink once the wait is slow.
98/// Why:
99/// Brief contention stays silent, as the incumbent's waits were;
100/// a long wait stops looking like a hang.
101///
102/// In TS you'd write (pseudocode):
103/// ```ts
104/// const onSlow = (waitedMs) => sink.write(slowWaitLine(...))
105/// ```
106pub struct HolderNotice<Sink: Write> {
107 /// The lock being waited for.
108 lock_directory: PathBuf,
109 /// What the lock protects, in words.
110 subject: &'static str,
111 /// The limit the wait gives up at.
112 limit: Duration,
113 /// Where the line goes.
114 sink: Sink,
115}
116
117impl<Sink: Write> HolderNotice<Sink> {
118 /// A notice for one lock, subject and limit.
119 pub fn new(lock_directory: &Path, subject: &'static str, limit: Duration, sink: Sink) -> Self {
120 return Self {
121 lock_directory: lock_directory.to_path_buf(),
122 subject,
123 limit,
124 sink,
125 };
126 }
127}
128
129impl<Sink: Write> WaitNotice for HolderNotice<Sink> {
130 /// The first busy attempt says nothing: most waits end within moments.
131 fn waiting(&mut self) {}
132
133 /// Print the slow-wait line naming the holder when its record can be read.
134 fn slow(&mut self, waited: Duration) {
135 let holder_pid: Option<i64> = match read_owner_lock_record(&self.lock_directory) {
136 Ok(PublishedOwner::Owner(record)) => Some(record.owner_pid),
137 Ok(PublishedOwner::Busy) | Err(_) => None,
138 };
139 let line: String = slow_wait_line(
140 self.subject,
141 &self.lock_directory,
142 holder_pid,
143 waited,
144 self.limit,
145 );
146 // Terminal output error: the notice is advisory; a failed write must not stop the wait.
147 let _written = writeln!(self.sink, "{line}");
148 }
149}
150
151/// What:
152/// Take an owner lock, waiting for a live holder with a slow-wait notice on standard error,
153/// and giving up at this process's wait limit.
154/// Why:
155/// One call replaces the silent unbounded wait at every lock a command may queue behind.
156///
157/// In TS you'd write (pseudocode):
158/// ```ts
159/// await acquireLiveOwnerLock({ lockDirectory, subject });
160/// ```
161pub fn acquire_live_owner_lock(
162 lock_directory: &Path,
163 subject: &'static str,
164) -> Result<OwnerLock, OwnerLockError> {
165 let limit: Duration = live_wait_limit();
166 let mut notice: HolderNotice<std::io::Stderr> =
167 HolderNotice::new(lock_directory, subject, limit, std::io::stderr());
168 return acquire_owner_lock_for(lock_directory, DEFAULT_POLL_DELAY, &mut notice, limit);
169}
170
171/// Wait notices and limits are controlled in the test module.
172#[cfg(test)]
173#[path = "live_lock_tests.rs"]
174mod tests;