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;