Skip to main content

ryra_vm/
progress.rs

1//! Standardized heartbeat for poll-until-ready loops.
2//!
3//! Every "wait for X to become ready" loop in ryra has the same shape: probe a
4//! condition on a fixed interval, bounded by a timeout, and bail if the timeout
5//! trips. Left silent, a slow wait reads as a hang. [`WaitProgress`] gives all
6//! of them one consistent line — *what* is being probed, *how many* checks have
7//! run, and the *limit* — printed on a throttled cadence so fast waits stay
8//! quiet while slow ones always show life.
9
10use std::time::{Duration, Instant};
11
12/// Tracks one poll-until-ready loop: owns the clock, counts probes, and emits a
13/// throttled heartbeat line.
14///
15/// Construct it just before the loop, then per iteration use [`timed_out`] for
16/// the bound and [`tick`] to count the probe and print a heartbeat when due:
17///
18/// ```ignore
19/// let mut progress = WaitProgress::new("vikunja", "systemctl + healthcheck", timeout)
20///     .with_prefix(format!("[{test_name}]     "));
21/// loop {
22///     if ready { return Ok(()); }
23///     if failed { bail!("..."); }
24///     if progress.timed_out() { bail!("not ready after {}s", timeout.as_secs()); }
25///     progress.tick();
26///     tokio::time::sleep(interval).await;
27/// }
28/// ```
29///
30/// [`timed_out`]: WaitProgress::timed_out
31/// [`tick`]: WaitProgress::tick
32pub struct WaitProgress {
33    /// What we're waiting for, e.g. a service name.
34    label: String,
35    /// How readiness is probed, e.g. "systemctl is-active" — the "how it
36    /// checks" the heartbeat reports.
37    method: String,
38    /// Line prefix so the heartbeat lines up with surrounding output.
39    prefix: String,
40    /// Bound on the loop.
41    timeout: Duration,
42    /// Minimum gap between heartbeat lines.
43    heartbeat: Duration,
44    /// When the loop started — the single clock for `elapsed`/`timed_out`.
45    start: Instant,
46    /// When the last heartbeat printed (starts at `start`, so the first line
47    /// waits a full `heartbeat` — fast waits print nothing).
48    last_logged: Instant,
49    /// How many probes have run.
50    checks: u32,
51}
52
53impl WaitProgress {
54    /// `label` is what we're waiting for; `method` describes how readiness is
55    /// probed; the loop is bounded by `timeout`. Defaults to a two-space prefix
56    /// and a 5s heartbeat cadence — override with [`with_prefix`] /
57    /// [`with_heartbeat`].
58    ///
59    /// [`with_prefix`]: WaitProgress::with_prefix
60    /// [`with_heartbeat`]: WaitProgress::with_heartbeat
61    pub fn new(label: impl Into<String>, method: impl Into<String>, timeout: Duration) -> Self {
62        let start = Instant::now();
63        Self {
64            label: label.into(),
65            method: method.into(),
66            prefix: "  ".to_string(),
67            timeout,
68            heartbeat: Duration::from_secs(5),
69            start,
70            last_logged: start,
71            checks: 0,
72        }
73    }
74
75    /// Set the line prefix so the heartbeat aligns with the surrounding output
76    /// (e.g. `"[my-test]     "`). Defaults to two spaces.
77    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
78        self.prefix = prefix.into();
79        self
80    }
81
82    /// Override how often a heartbeat prints. Defaults to 5s — right for the
83    /// short service/port waits; long VM boot/SSH waits set this to 30s so they
84    /// don't flood the log.
85    pub fn with_heartbeat(mut self, every: Duration) -> Self {
86        self.heartbeat = every;
87        self
88    }
89
90    /// Time since the loop started.
91    pub fn elapsed(&self) -> Duration {
92        self.start.elapsed()
93    }
94
95    /// Whether the loop has run past its timeout.
96    pub fn timed_out(&self) -> bool {
97        self.start.elapsed() > self.timeout
98    }
99
100    /// Record one probe attempt and print a heartbeat line if the cadence is
101    /// due. Call once per loop iteration.
102    pub fn tick(&mut self) {
103        self.checks += 1;
104        if self.last_logged.elapsed() >= self.heartbeat {
105            println!(
106                "{}still waiting for {}... via {} (check {}, limit {}s)",
107                self.prefix,
108                self.label,
109                self.method,
110                self.checks,
111                self.timeout.as_secs(),
112            );
113            self.last_logged = Instant::now();
114        }
115    }
116}