git_policy_cli/child_watch.rs
1//! What:
2//! A watcher for a wait that blocks inside a read or a wait on a child process:
3//! it prints a notice after the grace and at every interval while the wait lasts,
4//! and stops the child when the wait crosses its bound.
5//! Why:
6//! The wrapper reads Git objects from a long-lived `git cat-file --batch` and runs Git steps of a commit
7//! with their output captured.
8//! Both block in the operating system with nothing printed and no end,
9//! so a stalled child looks the same as a slow one and holds the commit for good (issue 1811).
10//! A watcher thread sees the wait from outside:
11//! the waiting thread marks where a wait begins and ends,
12//! the watcher prints, and at the bound it ends the child so the blocked call returns.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! const watch = startWatch({ waitingFor, pid, limitMs, cadence, notices });
17//! watch.begin(); try { await child.read(); } finally { watch.end(); }
18//! if (watch.expired) throw new Error(`${waitingFor} did not finish within ${limit} seconds`);
19//! ```
20
21/// The bound variable shared by every class of step, and the classes themselves.
22use super::child_class::{CHILD_WAIT_VARIABLE, ChildClass};
23/// Starts that ride out a short shortage of processes.
24use super::start_retry::RetryStart;
25/// The notice cadence, its ticker and the child-wait line.
26use super::wait_progress::{Cadence, ChildWaitFacts, Ticker, child_wait_line};
27/// The child and command types of the one-shot run.
28use std::process::{Child, Command, Output, Stdio};
29/// The sink a notice goes to.
30use std::io::Write;
31/// Shared state between the waiting thread and the watcher.
32use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
33/// The watcher thread.
34use std::thread::JoinHandle;
35/// Time spans and the monotonic clock.
36use std::time::{Duration, Instant};
37
38/// The shortest sleep of the watcher, so a due moment that already passed cannot spin it.
39const SHORTEST_SLEEP: Duration = Duration::from_millis(1);
40
41/// What:
42/// When a watched wait prints and where it stops the child.
43/// Why:
44/// The production entry reads these from the environment and writes to standard error;
45/// tests choose short ones and read the notices from a buffer.
46///
47/// In TS you'd write (pseudocode):
48/// ```ts
49/// type WatchTerms = { limitMs: number; cadence: Cadence; notices: Writable };
50/// ```
51pub struct WatchTerms {
52 /// How long one wait may last before the child is stopped.
53 pub limit: Duration,
54 /// When the notices of a wait are due.
55 pub cadence: Cadence,
56 /// Where the notices go.
57 pub notices: Box<dyn Write + Send>,
58}
59
60impl WatchTerms {
61 /// The terms a step of `class` runs under: the class's bound (or the variable's) and the class's grace before
62 /// the first notice, the notice interval of the environment, notices on standard error.
63 /// The grace never exceeds the interval, so a short interval still announces a wait quickly, nor half the bound,
64 /// so a bound set below the class's grace still gets a notice before the step is stopped.
65 pub fn for_class(class: ChildClass) -> Self {
66 let interval: Cadence = Cadence::from_environment();
67 let limit: Duration = class.limit();
68 return Self {
69 limit,
70 cadence: Cadence { grace: grace_for(class, interval.interval, limit), interval: interval.interval },
71 notices: Box::new(std::io::stderr()),
72 };
73 }
74
75 /// The terms of a step that reads objects through a long-lived reader: those of an object-writing step, the longer
76 /// class, because one request can name a large blob.
77 pub fn from_environment() -> Self {
78 return Self::for_class(ChildClass::Object);
79 }
80}
81
82/// What:
83/// How long a step of `class` runs before its first notice, for a notice `interval` and a `limit`.
84/// Why:
85/// The class's grace, but never past the interval (a short interval still announces a wait quickly) and never past
86/// half the bound (a bound set below the grace still gets a notice before the step is stopped).
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// Math.min(class.noticeGrace, interval, limit / 2)
91/// ```
92pub fn grace_for(class: ChildClass, interval: Duration, limit: Duration) -> Duration {
93 return class.notice_grace().min(interval).min(limit / 2);
94}
95
96/// What:
97/// What the watcher thread shares with the waiting thread, behind one lock.
98/// Why:
99/// The waiting thread marks a wait's start and end; the watcher reads the marks and the clock.
100///
101/// In TS you'd write (pseudocode):
102/// ```ts
103/// type WatchState = { since?: number; ticker: Ticker; printed: number; stopped: boolean };
104/// ```
105struct WatchState {
106 /// When the current wait began, or nothing between waits.
107 since: Option<Instant>,
108 /// When the current wait's next notice is due.
109 ticker: Ticker,
110 /// How many notices the current wait printed.
111 printed: u32,
112 /// Whether the watch was dropped.
113 stopped: bool,
114 /// Whether a wait crossed the bound and the child was stopped.
115 expired: bool,
116}
117
118/// The lock and the signal the two threads share.
119struct Shared {
120 /// The marks.
121 state: Mutex<WatchState>,
122 /// Signalled when a wait begins or ends or the watch stops.
123 wake: Condvar,
124}
125
126/// Take the lock, ignoring poison: the state is plain marks that stay valid after a panic elsewhere.
127fn locked(shared: &Shared) -> MutexGuard<'_, WatchState> {
128 return shared.state.lock().unwrap_or_else(PoisonError::into_inner);
129}
130
131/// What:
132/// Ends a process the way a watched wait does when it crosses its bound.
133/// Why:
134/// The blocked read or wait on that process then returns, and the waiting thread reports the bound.
135///
136/// In TS you'd write (pseudocode):
137/// ```ts
138/// process.kill(pid, 'SIGKILL');
139/// ```
140#[cfg(unix)]
141fn stop_process(pid: u32) {
142 let Ok(raw) = i32::try_from(pid) else {
143 return;
144 };
145 // SAFETY: `kill` takes no pointers. The watcher fires only while a wait is marked, and the waiting thread reaps
146 // the child only after it ends the mark, except for the window between a one-shot run's `wait_with_output`
147 // returning and its `end` call (microseconds), where a reaped identifier could in principle be reused by another
148 // process before the signal lands; the bound is far longer than that window, so a stop in it needs a wait that
149 // crossed the bound in the same instant.
150 unsafe {
151 libc::kill(raw, libc::SIGKILL);
152 }
153}
154
155/// A platform without signals cannot end the child from the watcher; the wait is only announced.
156// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
157#[cfg(not(unix))]
158fn stop_process(_pid: u32) {}
159
160/// What:
161/// The watcher of one child process.
162/// Why:
163/// One watch serves every wait on the same child:
164/// a long-lived reader marks one wait per request, a one-shot command marks one wait.
165/// Dropping the watch ends its thread.
166///
167/// In TS you'd write (pseudocode):
168/// ```ts
169/// class ChildWatch implements Disposable { begin(); end(); get expired(): boolean }
170/// ```
171pub struct ChildWatch {
172 /// The state shared with the watcher thread.
173 shared: Arc<Shared>,
174 /// The watcher thread, joined when the watch is dropped.
175 thread: Option<JoinHandle<()>>,
176 /// The bound a wait may last.
177 limit: Duration,
178 /// When the notices of each wait are due.
179 cadence: Cadence,
180}
181
182impl ChildWatch {
183 /// Start watching `pid`:
184 /// waits marked with `begin` and `end` are announced as waiting for `waiting_for` and bounded by the terms' limit.
185 /// A watch whose thread cannot start watches nothing, and the waits it marks run unannounced and unbounded.
186 pub fn start(waiting_for: &str, pid: u32, terms: WatchTerms) -> Self {
187 let shared: Arc<Shared> = Arc::new(Shared {
188 state: Mutex::new(WatchState {
189 since: None,
190 ticker: Ticker::new(terms.cadence),
191 printed: 0,
192 stopped: false,
193 expired: false,
194 }),
195 wake: Condvar::new(),
196 });
197 let watcher: Arc<Shared> = Arc::clone(&shared);
198 let subject: String = String::from(waiting_for);
199 let limit: Duration = terms.limit;
200 let cadence: Cadence = terms.cadence;
201 let thread: Option<JoinHandle<()>> = std::thread::Builder::new()
202 .name(String::from("cli-git-child-watch"))
203 .spawn(move || {
204 watch_loop(&watcher, subject.as_str(), pid, terms);
205 })
206 .ok();
207 return Self { shared, thread, limit, cadence };
208 }
209
210 /// Mark the start of a wait on the child.
211 pub fn begin(&self) {
212 let mut state: MutexGuard<'_, WatchState> = locked(&self.shared);
213 state.since = Some(Instant::now());
214 state.ticker = Ticker::new(self.cadence);
215 state.printed = 0;
216 drop(state);
217 self.shared.wake.notify_all();
218 }
219
220 /// Mark the end of the wait.
221 pub fn end(&self) {
222 let mut state: MutexGuard<'_, WatchState> = locked(&self.shared);
223 state.since = None;
224 drop(state);
225 self.shared.wake.notify_all();
226 }
227
228 /// Whether a wait crossed the bound and the child was stopped.
229 pub fn expired(&self) -> bool {
230 return locked(&self.shared).expired;
231 }
232
233 /// The bound a wait may last.
234 pub fn limit(&self) -> Duration {
235 return self.limit;
236 }
237}
238
239/// Stops the watcher thread and waits for it.
240impl Drop for ChildWatch {
241 fn drop(&mut self) {
242 locked(&self.shared).stopped = true;
243 self.shared.wake.notify_all();
244 if let Some(thread) = self.thread.take() {
245 // A watcher that panicked has nothing left to report; the wait it watched is over.
246 let _joined = thread.join();
247 }
248 }
249}
250
251/// What:
252/// The watcher thread's loop: sleep until the next notice or the bound is due, print or stop, repeat.
253/// Why:
254/// Between waits the thread sleeps without a timeout, so an idle watch costs nothing.
255///
256/// In TS you'd write (pseudocode):
257/// ```ts
258/// while (!stopped) { if (since === undefined) await signal(); else if (elapsed >= limit) stop(pid); else if (ticker.due(elapsed)) notice(); else await sleep(next); }
259/// ```
260fn watch_loop(shared: &Shared, waiting_for: &str, pid: u32, mut terms: WatchTerms) {
261 let mut state: MutexGuard<'_, WatchState> = locked(shared);
262 loop {
263 if state.stopped {
264 return;
265 }
266 let Some(since) = state.since else {
267 state = shared.wake.wait(state).unwrap_or_else(PoisonError::into_inner);
268 continue;
269 };
270 let mut waited: Duration = since.elapsed();
271 // A notice that is due goes out before the bound is checked, so a child stopped at its bound was announced.
272 if state.ticker.due(waited) {
273 let line: String = child_wait_line(&ChildWaitFacts {
274 waiting_for,
275 pid,
276 waited,
277 limit: terms.limit,
278 limit_note: " (CLI_GIT_CHILD_WAIT_SECONDS changes this)",
279 repeat: state.printed > 0,
280 });
281 state.printed += 1;
282 // The lock is not held while writing: a slow terminal must not hold up the waiting thread's marks.
283 drop(state);
284 // Terminal output error: a notice is advisory; a failed write must not stop the watch.
285 let _written = writeln!(terms.notices, "{line}");
286 state = locked(shared);
287 // The wait may have ended, or another begun, while the line was written.
288 if state.stopped || state.since != Some(since) {
289 continue;
290 }
291 // The bound is checked right after a notice, however long the notice took, so printing can never keep a
292 // child past its bound.
293 waited = since.elapsed();
294 }
295 if waited >= terms.limit {
296 state.expired = true;
297 state.since = None;
298 stop_process(pid);
299 continue;
300 }
301 let until_notice: Duration = state.ticker.next_in(waited);
302 let until_bound: Duration = terms.limit.saturating_sub(waited);
303 let sleep: Duration = until_notice.min(until_bound).max(SHORTEST_SLEEP);
304 state = shared
305 .wake
306 .wait_timeout(state, sleep)
307 .unwrap_or_else(PoisonError::into_inner)
308 .0;
309 }
310}
311
312/// What:
313/// How the object reader's wait names itself in notices and in the timeout sentence.
314/// Why:
315/// The reader is one long-lived `cat-file --batch` whose single request can name a large blob, so it runs under the
316/// bulk class on purpose (a `cat-file` that answers one query through `run_git` is a quick query step), and its
317/// subject says so like every other step.
318///
319/// In TS you'd write (pseudocode):
320/// ```ts
321/// `git cat-file --batch, ${labelOf(BULK)}`
322/// ```
323pub fn reader_subject() -> String {
324 return format!("git cat-file --batch, {}", ChildClass::Object.label());
325}
326
327/// What:
328/// The sentence for a watched wait that crossed its bound.
329/// Why:
330/// It names what was waited for, the bound, where to change it and what to do next.
331///
332/// In TS you'd write (pseudocode):
333/// ```ts
334/// `${waitingFor} did not finish within ${limit} seconds and was stopped ...`
335/// ```
336pub fn watched_timeout_text(waiting_for: &str, limit: Duration) -> String {
337 return format!(
338 "{waiting_for} did not finish within {} seconds and was stopped ({CHILD_WAIT_VARIABLE} changes this). \
339 If the step belonged to a commit, the next Git command of this wrapper finishes or undoes that commit, so \
340 run the command again. If the step was only slow, raise the bound with {CHILD_WAIT_VARIABLE}=<seconds>; \
341 if it stalls again, check with `ps` what that process waits for.",
342 limit.as_secs()
343 );
344}
345
346/// What:
347/// Run a command to its end and collect its output, announcing the wait and stopping the child at the bound.
348/// The command's input must be set by the caller;
349/// its output streams are replaced by pipes.
350/// Why:
351/// `Command::output` blocks until the child ends and prints nothing meanwhile.
352/// A Git step that stalls under load or behind a filter must say so, and must not hold a commit for good.
353///
354/// In TS you'd write (pseudocode):
355/// ```ts
356/// const output = await spawnWatched(command, { waitingFor, terms }); // throws on the bound
357/// ```
358pub fn output_watched(command: &mut Command, waiting_for: &str, terms: WatchTerms) -> std::io::Result<Output> {
359 command.stdout(Stdio::piped()).stderr(Stdio::piped());
360 let child: Child = command.spawn_retrying()?;
361 let watch: ChildWatch = ChildWatch::start(waiting_for, child.id(), terms);
362 watch.begin();
363 let output: std::io::Result<Output> = child.wait_with_output();
364 watch.end();
365 if watch.expired() {
366 return Err(std::io::Error::new(
367 std::io::ErrorKind::TimedOut,
368 watched_timeout_text(waiting_for, watch.limit()),
369 ));
370 }
371 return output;
372}
373
374/// Watch controls stay out of the release executable.
375#[cfg(test)]
376#[path = "child_watch_tests.rs"]
377mod tests;