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/// A commit's place among the commits that wait for the same thing.
191/// Why:
192/// The reservation registry knows the order in which waiting commits will be served,
193/// and a commit in line is told its number.
194///
195/// In TS you'd write (pseudocode):
196/// ```ts
197/// type QueuePlace = { position: number; waiting: number };
198/// ```
199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
200pub struct QueuePlace {
201 /// This commit's number, counted from 1;
202 /// the first is served next.
203 pub position: usize,
204 /// How many commits are in line, this one included.
205 pub waiting: usize,
206}
207
208/// What:
209/// The sentence that tells a commit its place.
210/// Why:
211/// The line says how many commits are served before this one.
212///
213/// In TS you'd write (pseudocode):
214/// ```ts
215/// `This is number ${position} of ${waiting} commits in line.`
216/// ```
217pub fn queue_place_text(place: QueuePlace) -> String {
218 let ahead: usize = place.position.saturating_sub(1);
219 return format!(
220 "This is number {} of {} commits in line, so {ahead} will be served first.",
221 place.position, place.waiting
222 );
223}
224
225/// What:
226/// What one notice of a wait for a child process says.
227/// Why:
228/// A child whose output goes to a file or a pipe is silent while it runs,
229/// so the wait names what it waits for, the child's process, how long it has lasted and where it stops the child.
230///
231/// In TS you'd write (pseudocode):
232/// ```ts
233/// type ChildWaitFacts = { waitingFor; pid; waitedMs; limitMs; limitNote; repeat };
234/// ```
235#[derive(Clone, Copy, Debug)]
236pub struct ChildWaitFacts<'facts> {
237 /// What the wait is for, in words ("monochromatic-lint to check a.md").
238 pub waiting_for: &'facts str,
239 /// The child's process.
240 pub pid: u32,
241 /// How long the wait has lasted.
242 pub waited: Duration,
243 /// How long the child may run before it is stopped.
244 pub limit: Duration,
245 /// Where the person changes the limit, spelled to follow "seconds", or empty when nothing changes it.
246 pub limit_note: &'facts str,
247 /// Whether an earlier notice of this wait already printed the hint.
248 pub repeat: bool,
249}
250
251/// What:
252/// The notice of a wait for a child process.
253/// Why:
254/// The first notice of a wait adds what to do if the child is stuck, and the repeats stay short.
255///
256/// In TS you'd write (pseudocode):
257/// ```ts
258/// `cli-git: still waiting for ${waitingFor} (process ${pid}) after ${waited} seconds; stopping it after ${limit} seconds`
259/// ```
260pub fn child_wait_line(facts: &ChildWaitFacts<'_>) -> String {
261 let hint: &str = if facts.repeat {
262 ""
263 } else {
264 " Hint: it is still running; if it looks stuck, check it with `ps` and report it."
265 };
266 return format!(
267 "cli-git: still waiting for {} (process {}) after {} seconds; stopping it after {} seconds{}.{hint}",
268 facts.waiting_for,
269 facts.pid,
270 facts.waited.as_secs(),
271 facts.limit.as_secs(),
272 facts.limit_note
273 );
274}
275
276/// What:
277/// Prints a wait's notices to a sink at the pace of its ticker.
278/// Why:
279/// Wait loops that poll (reservations, child processes, settlement locks) share one printing rule:
280/// the line is built only when it is due,
281/// so a poll that is not due costs a clock read and nothing else.
282///
283/// In TS you'd write (pseudocode):
284/// ```ts
285/// const reporter = new WaitReporter(cadence, stderr); reporter.poll((waitedMs) => line(waitedMs));
286/// ```
287pub struct WaitReporter<'sink> {
288 /// When the wait began.
289 started: Instant,
290 /// Decides when a line is due.
291 ticker: Ticker,
292 /// Where the lines go.
293 sink: &'sink mut dyn Write,
294 /// How many lines were printed.
295 printed: u32,
296}
297
298impl<'sink> WaitReporter<'sink> {
299 /// A reporter whose clock starts now and whose first notice follows the cadence's grace.
300 pub fn start(cadence: Cadence, sink: &'sink mut dyn Write) -> Self {
301 return Self {
302 started: Instant::now(),
303 ticker: Ticker::new(cadence),
304 sink,
305 printed: 0,
306 };
307 }
308
309 /// Print the line built by `line` when a notice is due;
310 /// `line` gets how long the wait has lasted and whether an earlier notice of this wait was printed.
311 /// Whether one was printed.
312 pub fn poll(&mut self, line: impl FnOnce(Duration, bool) -> String) -> bool {
313 let waited: Duration = self.started.elapsed();
314 if !self.ticker.due(waited) {
315 return false;
316 }
317 let text: String = line(waited, self.printed > 0);
318 self.printed += 1;
319 // Terminal output error: a notice is advisory; a failed write must not stop the wait.
320 let _written = writeln!(self.sink, "{text}");
321 return true;
322 }
323}
324
325/// Cadence, ticker and line controls stay out of the release executable.
326#[cfg(test)]
327#[path = "wait_progress_tests.rs"]
328mod tests;