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///  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;