1use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::{Mutex, PoisonError};
12
13use tokio::time::{Duration, Instant};
14
15pub const SCRIPT_GIVE_UP: Duration = Duration::from_mins(10);
17
18#[derive(Debug)]
24pub struct ProgressClock {
25 last: Mutex<Instant>,
26 holds: AtomicUsize,
27}
28
29impl ProgressClock {
30 #[must_use]
32 pub fn new() -> Self {
33 Self {
34 last: Mutex::new(Instant::now()),
35 holds: AtomicUsize::new(0),
36 }
37 }
38
39 pub fn tick(&self) {
41 *self.last.lock().unwrap_or_else(PoisonError::into_inner) = Instant::now();
42 }
43
44 #[must_use]
46 pub fn last(&self) -> Instant {
47 if self.holds.load(Ordering::Acquire) > 0 {
48 return Instant::now();
49 }
50 *self.last.lock().unwrap_or_else(PoisonError::into_inner)
51 }
52
53 #[must_use]
56 pub fn hold(&self) -> ClockHold<'_> {
57 self.holds.fetch_add(1, Ordering::AcqRel);
58 ClockHold(self)
59 }
60}
61
62#[derive(Debug)]
64pub struct ClockHold<'a>(&'a ProgressClock);
65
66impl Drop for ClockHold<'_> {
67 fn drop(&mut self) {
68 self.0.tick();
69 self.0.holds.fetch_sub(1, Ordering::AcqRel);
70 }
71}
72
73impl Default for ProgressClock {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct StopPolicy {
82 pub give_up_after: Option<Duration>,
84 pub clock: std::sync::Arc<ProgressClock>,
86}
87
88impl StopPolicy {
89 #[must_use]
92 pub fn new(
93 is_terminal: bool,
94 give_up_after: Option<Duration>,
95 clock: std::sync::Arc<ProgressClock>,
96 ) -> Self {
97 let give_up_after = give_up_after.or((!is_terminal).then_some(SCRIPT_GIVE_UP));
98 Self {
99 give_up_after,
100 clock,
101 }
102 }
103
104 pub async fn expired(&self) -> GaveUp {
107 let Some(limit) = self.give_up_after else {
108 return std::future::pending().await;
109 };
110 loop {
111 let deadline = self.clock.last() + limit;
112 tokio::time::sleep_until(deadline).await;
113 if self.clock.last() + limit <= Instant::now() {
114 return GaveUp { idle: limit };
115 }
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub struct GaveUp {
123 pub idle: Duration,
125}
126
127impl std::fmt::Display for GaveUp {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 let secs = self.idle.as_secs();
130 if secs >= 60 && secs.is_multiple_of(60) {
131 write!(f, "no progress for {}m; rerun to resume", secs / 60)
132 } else {
133 write!(f, "no progress for {secs}s; rerun to resume")
134 }
135 }
136}
137
138impl std::error::Error for GaveUp {}
139
140#[cfg(test)]
141mod tests;