Skip to main content

decdn_client/
stop.rs

1//! When a command gives up (ADR 039 ยง Failure handling: reassign-only tail,
2//! the stop policy).
3//!
4//! The clock measures time since the last verified byte anywhere in the
5//! command, never time since the start. A command in a terminal has no limit:
6//! the human watches the bar and presses Ctrl-C. A script gets
7//! [`SCRIPT_GIVE_UP`](crate::stop::SCRIPT_GIVE_UP). `--give-up-after-secs`
8//! overrides both.
9
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::{Mutex, PoisonError};
12
13use tokio::time::{Duration, Instant};
14use tokio_util::sync::CancellationToken;
15
16/// The no-progress limit when stderr is not a terminal.
17pub const SCRIPT_GIVE_UP: Duration = Duration::from_mins(10);
18
19/// The instant of the command's last verified byte.
20///
21/// A [`ClockHold`] stops the clock: while any hold is alive the command counts
22/// as making progress, for time spent waiting on the command's own consumer
23/// rather than on a source.
24#[derive(Debug)]
25pub struct ProgressClock {
26    last: Mutex<Instant>,
27    holds: AtomicUsize,
28}
29
30impl ProgressClock {
31    /// A clock whose last progress is now.
32    #[must_use]
33    pub fn new() -> Self {
34        Self {
35            last: Mutex::new(Instant::now()),
36            holds: AtomicUsize::new(0),
37        }
38    }
39
40    /// Record a verified byte now.
41    pub fn tick(&self) {
42        *self.last.lock().unwrap_or_else(PoisonError::into_inner) = Instant::now();
43    }
44
45    /// The instant of the last verified byte, or now while a hold is alive.
46    #[must_use]
47    pub fn last(&self) -> Instant {
48        if self.holds.load(Ordering::Acquire) > 0 {
49            return Instant::now();
50        }
51        *self.last.lock().unwrap_or_else(PoisonError::into_inner)
52    }
53
54    /// Stop the clock until the returned hold drops. The drop counts as
55    /// progress, so the no-progress limit restarts from it.
56    #[must_use]
57    pub fn hold(&self) -> ClockHold<'_> {
58        self.holds.fetch_add(1, Ordering::AcqRel);
59        ClockHold(self)
60    }
61}
62
63/// Stops a [`ProgressClock`] while it lives. See [`ProgressClock::hold`].
64#[derive(Debug)]
65pub struct ClockHold<'a>(&'a ProgressClock);
66
67impl Drop for ClockHold<'_> {
68    fn drop(&mut self) {
69        self.0.tick();
70        self.0.holds.fetch_sub(1, Ordering::AcqRel);
71    }
72}
73
74impl Default for ProgressClock {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80/// When the command stops for lack of progress.
81///
82/// A bundle pull also halts it: once one entry ends the pull, every sibling
83/// entry's fetch stops at its next wait or loop pass and lets its lane builds
84/// drain, rather than being dropped in the middle of one. A recovery step in
85/// progress runs to its outcome first. A clone shares the halt, which holds
86/// for one pass of the pull.
87#[derive(Debug, Clone)]
88pub struct StopPolicy {
89    /// The no-progress limit. `None` waits until the caller interrupts.
90    pub give_up_after: Option<Duration>,
91    /// The command-wide progress clock.
92    pub clock: std::sync::Arc<ProgressClock>,
93    /// Cancelled once a sibling entry ends this pass of the pull. A clone
94    /// shares it; [`StopPolicy::next_pass`] starts a fresh one.
95    halt: CancellationToken,
96}
97
98impl StopPolicy {
99    /// The policy for a command whose stderr `is_terminal`, with the user's
100    /// optional `--give-up-after-secs` override.
101    #[must_use]
102    pub fn new(
103        is_terminal: bool,
104        give_up_after: Option<Duration>,
105        clock: std::sync::Arc<ProgressClock>,
106    ) -> Self {
107        let give_up_after = give_up_after.or((!is_terminal).then_some(SCRIPT_GIVE_UP));
108        Self {
109            give_up_after,
110            clock,
111            halt: CancellationToken::new(),
112        }
113    }
114
115    /// The same policy, on the same clock, for the next pass of a pull: a
116    /// halt of this pass does not carry over.
117    #[must_use]
118    pub(crate) fn next_pass(&self) -> Self {
119        Self {
120            give_up_after: self.give_up_after,
121            clock: std::sync::Arc::clone(&self.clock),
122            halt: CancellationToken::new(),
123        }
124    }
125
126    /// Halt every fetch on this policy at its next wait or loop pass.
127    pub(crate) fn halt(&self) {
128        self.halt.cancel();
129    }
130
131    /// Whether this policy is halted.
132    pub(crate) fn is_halted(&self) -> bool {
133        self.halt.is_cancelled()
134    }
135
136    /// `Ok` until this policy is halted.
137    ///
138    /// # Errors
139    ///
140    /// [`Halted`] once it is.
141    pub(crate) fn ensure_running(&self) -> Result<(), Halted> {
142        if self.is_halted() {
143            Err(Halted)
144        } else {
145            Ok(())
146        }
147    }
148
149    /// Resolve once this policy is halted.
150    pub(crate) async fn halted(&self) -> Halted {
151        self.halt.cancelled().await;
152        Halted
153    }
154
155    /// Resolve once the command has made no progress for the limit. Never
156    /// resolves without a limit.
157    pub async fn expired(&self) -> GaveUp {
158        let Some(limit) = self.give_up_after else {
159            return std::future::pending().await;
160        };
161        loop {
162            let deadline = self.clock.last() + limit;
163            tokio::time::sleep_until(deadline).await;
164            if self.clock.last() + limit <= Instant::now() {
165                return GaveUp { idle: limit };
166            }
167        }
168    }
169}
170
171/// The command made no progress for `idle` and stopped. A rerun resumes.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub struct GaveUp {
174    /// How long the command went without a verified byte.
175    pub idle: Duration,
176}
177
178impl std::fmt::Display for GaveUp {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        let secs = self.idle.as_secs();
181        if secs >= 60 && secs.is_multiple_of(60) {
182            write!(f, "no progress for {}m; rerun to resume", secs / 60)
183        } else {
184            write!(f, "no progress for {secs}s; rerun to resume")
185        }
186    }
187}
188
189impl std::error::Error for GaveUp {}
190
191/// A sibling entry ended the pull, so this entry's fetch stopped
192/// ([`StopPolicy::halt`]). The pull ends with the sibling's error, so this
193/// one reaches a caller only as the text a late lane build's escrow folds in.
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub(crate) struct Halted;
196
197impl std::fmt::Display for Halted {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        f.write_str("another entry ended the pull")
200    }
201}
202
203impl std::error::Error for Halted {}
204
205#[cfg(test)]
206mod tests;