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