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