Skip to main content

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;