Skip to main content

git_policy_cli/push/
wait_progress.rs

1//! What:
2//!  The shared clock of every wait notice:
3//!  when the first line of a wait is due,
4//!  how often it repeats,
5//!  and the place in line a queued commit holds.
6//! Why:
7//!  A commit that queues behind other commits, a lock, a reservation or a child process must say so,
8//!  and say it again at a steady interval,
9//!  or the caller cannot tell a queued commit from a hung one (issue 1811).
10//!  Each wait loop feeds its elapsed time to a `Ticker` and prints a line when the ticker says one is due,
11//!  so the decision is a function of elapsed time alone and a test can drive it with chosen durations.
12//!  The interval comes from `CLI_GIT_WAIT_NOTICE_SECONDS` at the production entry of each wait.
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! const ticker = new Ticker(Cadence.fromEnvironment());
17//! while (waiting) { if (ticker.due(elapsedMs)) stderr.write(line(elapsedMs)); await wait(pollMs); }
18//! ```
19
20/// Positive-seconds parsing shared with the lock wait limits.
21use crate::bounded_push::positive_seconds;
22/// Borrowed OS text of the environment value.
23use std::ffi::OsStr;
24/// `Write` is the sink a notice goes to.
25use std::io::Write;
26/// Time spans and the monotonic clock.
27use std::time::{Duration, Instant};
28
29/// Environment variable that sets, in whole seconds, how often a waiting command repeats its notice.
30pub const NOTICE_INTERVAL_VARIABLE: &str = "CLI_GIT_WAIT_NOTICE_SECONDS";
31
32/// How often a waiting command repeats its notice when the variable is absent or unusable.
33///  Exempt from a citation under `LQM` (class: notice pacing): it times a line the person reads or a log threshold, and
34///  nothing waits on it beyond the wait that already exists. Classed once in `doc/planning/cli-git-shipped-limits-
35///  audit.md`, "Exempt limits"; the coordinator's pick under the owner's delegation, open to the owner's veto.
36pub const DEFAULT_NOTICE_INTERVAL: Duration = Duration::from_secs(10);
37
38/// How long a wait stays silent before its first notice:
39///  a landing that holds the lock normally ends within this,
40///  so brief contention between healthy commits prints nothing.
41///  Exempt from a citation under `LQM` (class: notice pacing): it times a line the person reads or a log threshold, and
42///  nothing waits on it beyond the wait that already exists. Classed once in `doc/planning/cli-git-shipped-limits-
43///  audit.md`, "Exempt limits"; the coordinator's pick under the owner's delegation, open to the owner's veto.
44pub const NOTICE_GRACE: Duration = Duration::from_secs(1);
45
46/// The shortest interval a ticker divides by, so a zero interval cannot divide by zero.
47const SHORTEST_INTERVAL: Duration = Duration::from_millis(1);
48
49/// What:
50///  When a wait's notices are due:
51///  the grace before the first and the interval between the rest.
52/// Why:
53///   One value travels from the production entry to the wait loop,
54///  and tests choose short ones instead of waiting out the defaults.
55///
56/// In TS you'd write (pseudocode):
57/// ```ts
58/// type Cadence = { graceMs: number; intervalMs: number };
59/// ```
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61pub struct Cadence {
62    /// Silence before the first notice.
63    pub grace: Duration,
64    /// Time between two notices.
65    pub interval: Duration,
66}
67
68impl Cadence {
69    /// The cadence for an interval:
70    ///  the standard grace, but never longer than the interval itself.
71    ///
72    /// In TS you'd write (pseudocode):
73    /// ```ts
74    /// { graceMs: Math.min(GRACE_MS, intervalMs), intervalMs }
75    /// ```
76    pub fn from_interval(interval: Duration) -> Self {
77        return Self {
78            grace: interval.min(NOTICE_GRACE),
79            interval,
80        };
81    }
82
83    /// The cadence when nothing configures it.
84    pub fn standard() -> Self {
85        return Self::from_interval(DEFAULT_NOTICE_INTERVAL);
86    }
87
88    /// The cadence for a value of the variable;
89    ///  a missing, zero or malformed value falls back to the standard one.
90    ///
91    /// In TS you'd write (pseudocode):
92    /// ```ts
93    /// Cadence.fromInterval(positiveSeconds(value) ?? DEFAULT_INTERVAL)
94    /// ```
95    pub fn from_value(value: Option<&OsStr>) -> Self {
96        let Some(text) = value else {
97            return Self::standard();
98        };
99        return match positive_seconds(NOTICE_INTERVAL_VARIABLE, text) {
100            Some(interval) => Self::from_interval(interval),
101            None => Self::standard(),
102        };
103    }
104
105    /// The cadence this process runs under:
106    ///  the variable in its environment, or the standard one.
107    pub fn from_environment() -> Self {
108        return Self::from_value(std::env::var_os(NOTICE_INTERVAL_VARIABLE).as_deref());
109    }
110}
111
112/// What:
113///  Decides, from elapsed time alone,
114///  whether a notice is due,
115///  and when the next one will be.
116/// Why:
117///   A wait loop polls at its own pace.
118///  The ticker answers "is a line due now" once per due notice,
119///  however late the poll comes,
120///  and never answers twice for the same interval.
121///
122/// In TS you'd write (pseudocode):
123/// ```ts
124/// class Ticker { due(elapsedMs: number): boolean; nextInMs(elapsedMs: number): number }
125/// ```
126#[derive(Clone, Copy, Debug)]
127pub struct Ticker {
128    /// The interval between notices.
129    interval: Duration,
130    /// When the first notice is due.
131    first_at: Duration,
132    /// How many notices were already due.
133    printed: u32,
134}
135
136impl Ticker {
137    /// A ticker whose first notice comes after the cadence's grace.
138    pub fn new(cadence: Cadence) -> Self {
139        return Self {
140            interval: cadence.interval.max(SHORTEST_INTERVAL),
141            first_at: cadence.grace,
142            printed: 0,
143        };
144    }
145
146    /// A ticker for a wait that printed its first line when it began:
147    ///  the next notice comes one interval later.
148    pub fn announced_at_start(cadence: Cadence) -> Self {
149        return Self {
150            interval: cadence.interval.max(SHORTEST_INTERVAL),
151            first_at: Duration::ZERO,
152            printed: 1,
153        };
154    }
155
156    /// How many notices are due after `waited`.
157    fn due_count(&self, waited: Duration) -> u32 {
158        if waited < self.first_at {
159            return 0;
160        }
161        let after_first: u128 = (waited - self.first_at).as_nanos() / self.interval.as_nanos();
162        return u32::try_from(after_first.saturating_add(1)).unwrap_or(u32::MAX);
163    }
164
165    /// Whether a notice is due now that `waited` has passed;
166    ///  true once per due notice.
167    ///
168    /// In TS you'd write (pseudocode):
169    /// ```ts
170    /// ticker.due(elapsedMs)
171    /// ```
172    pub fn due(&mut self, waited: Duration) -> bool {
173        let count: u32 = self.due_count(waited);
174        if count <= self.printed {
175            return false;
176        }
177        self.printed = count;
178        return true;
179    }
180
181    /// How long after `waited` the next notice is due;
182    ///  a watcher sleeps this long.
183    ///
184    /// In TS you'd write (pseudocode):
185    /// ```ts
186    /// ticker.nextInMs(elapsedMs)
187    /// ```
188    pub fn next_in(&self, waited: Duration) -> Duration {
189        // The notice after the `printed`th one is due `printed` intervals after the first.
190        let due_at: Duration = self.first_at.saturating_add(self.interval.saturating_mul(self.printed));
191        return due_at.saturating_sub(waited);
192    }
193}
194
195/// What:
196///  What one notice of a wait for a child process says.
197/// Why:
198///   A child whose output goes to a file or a pipe is silent while it runs,
199///  so the wait names what it waits for, the child's process, how long it has lasted and where it stops the child.
200///
201/// In TS you'd write (pseudocode):
202/// ```ts
203/// type ChildWaitFacts = { waitingFor; pid; waitedMs; limitMs; limitNote; repeat };
204/// ```
205#[derive(Clone, Copy, Debug)]
206pub struct ChildWaitFacts<'facts> {
207    /// What the wait is for, in words ("monochromatic-lint to check a.md").
208    pub waiting_for: &'facts str,
209    /// The child's process.
210    pub pid: u32,
211    /// How long the wait has lasted.
212    pub waited: Duration,
213    /// How long the child may show no sign of work before it is ended, when it may be ended.
214    pub limit: Duration,
215    /// Where the person changes the limit, spelled to follow "seconds", or empty when nothing changes it.
216    pub limit_note: &'facts str,
217    /// Whether an earlier notice of this wait already printed the hint.
218    pub repeat: bool,
219    /// Whether a child with no sign of work for the limit is ended; a step that may not be run twice is only waited for.
220    pub ends_when_quiet: bool,
221}
222
223/// What:
224///  The notice of a wait for a child process.
225/// Why:
226///   The first notice of a wait adds what to do if the child is stuck, and the repeats stay short.
227///
228/// In TS you'd write (pseudocode):
229/// ```ts
230/// `cli-git: still waiting for ${waitingFor} (process ${pid}) after ${waited} seconds; ending it only after ${limit} seconds with no sign of work`
231/// ```
232pub fn child_wait_line(facts: &ChildWaitFacts<'_>) -> String {
233    let hint: &str = if facts.repeat {
234        ""
235    } else {
236        " Hint: it is still running; if it looks stuck, check it with `ps` and report it."
237    };
238    // A child is ended for being quiet and never for being slow, and a step that may not be run twice is never ended.
239    if !facts.ends_when_quiet {
240        return format!(
241            "cli-git: still waiting for {} (process {}) after {} seconds; it is a step that may not be run twice, so it is \
242             waited for as long as it lives{}.{hint}",
243            facts.waiting_for,
244            facts.pid,
245            facts.waited.as_secs(),
246            facts.limit_note
247        );
248    }
249    return format!(
250        "cli-git: still waiting for {} (process {}) after {} seconds; it is ended only after {} seconds with no sign of work{}.{hint}",
251        facts.waiting_for,
252        facts.pid,
253        facts.waited.as_secs(),
254        facts.limit.as_secs(),
255        facts.limit_note
256    );
257}
258
259/// What:
260///  Prints a wait's notices to a sink at the pace of its ticker.
261/// Why:
262///   Wait loops that poll (reservations, child processes) share one printing rule:
263///  the line is built only when it is due,
264///  so a poll that is not due costs a clock read and nothing else.
265///
266/// In TS you'd write (pseudocode):
267/// ```ts
268/// const reporter = new WaitReporter(cadence, stderr); reporter.poll((waitedMs) => line(waitedMs));
269/// ```
270pub struct WaitReporter<'sink> {
271    /// When the wait began.
272    started: Instant,
273    /// Decides when a line is due.
274    ticker: Ticker,
275    /// Where the lines go.
276    sink: &'sink mut dyn Write,
277    /// How many lines were printed.
278    printed: u32,
279}
280
281impl<'sink> WaitReporter<'sink> {
282    /// A reporter whose clock starts now and whose first notice follows the cadence's grace.
283    pub fn start(cadence: Cadence, sink: &'sink mut dyn Write) -> Self {
284        return Self {
285            started: Instant::now(),
286            ticker: Ticker::new(cadence),
287            sink,
288            printed: 0,
289        };
290    }
291
292    /// Print the line built by `line` when a notice is due;
293    ///  `line` gets how long the wait has lasted and whether an earlier notice of this wait was printed.
294    ///  Whether one was printed.
295    pub fn poll(&mut self, line: impl FnOnce(Duration, bool) -> String) -> bool {
296        let waited: Duration = self.started.elapsed();
297        if !self.ticker.due(waited) {
298            return false;
299        }
300        let text: String = line(waited, self.printed > 0);
301        self.printed += 1;
302        // Terminal output error: a notice is advisory; a failed write must not stop the wait.
303        let _written = writeln!(self.sink, "{text}");
304        return true;
305    }
306}
307
308/// Cadence, ticker and line controls stay out of the release executable.
309#[cfg(test)]
310#[path = "wait_progress_tests.rs"]
311mod tests;