1use std::{future::Future, time::Duration};
12
13use tokio::time::{Instant, sleep_until, timeout_at};
14
15use crate::Result;
16
17#[derive(Clone, Copy, Debug)]
19pub struct PollOptions {
20 timeout: Duration,
21 interval: Duration,
22}
23
24impl PollOptions {
25 pub fn new(timeout: Duration) -> Self {
27 Self {
28 timeout,
29 interval: Duration::from_secs(2),
30 }
31 }
32
33 pub fn interval(mut self, interval: Duration) -> Self {
35 self.interval = interval;
36 self
37 }
38}
39
40impl Default for PollOptions {
41 fn default() -> Self {
43 Self::new(Duration::from_secs(180))
44 }
45}
46
47#[derive(Clone, Debug)]
50#[must_use]
51#[non_exhaustive]
52pub enum WaitOutcome<T> {
53 Finished(T),
54 TimedOut {
56 last: Option<T>,
57 },
58 Vanished {
62 last: T,
63 },
64}
65
66impl<T> WaitOutcome<T> {
67 pub fn finished(self) -> Option<T> {
71 match self {
72 Self::Finished(value) => Some(value),
73 Self::TimedOut { .. } | Self::Vanished { .. } => None,
74 }
75 }
76
77 pub fn last(&self) -> Option<&T> {
79 match self {
80 Self::Finished(value) | Self::Vanished { last: value } => Some(value),
81 Self::TimedOut { last } => last.as_ref(),
82 }
83 }
84}
85
86pub(crate) async fn wait<T, F, Fut>(
90 options: PollOptions,
91 mut observe: F,
92 finished: impl Fn(&T) -> bool,
93) -> Result<WaitOutcome<T>>
94where
95 F: FnMut() -> Fut,
96 Fut: Future<Output = Result<Option<T>>>,
97{
98 let deadline = Instant::now() + options.timeout;
99 let mut last = None;
100 loop {
101 if Instant::now() >= deadline {
102 return Ok(WaitOutcome::TimedOut { last });
103 }
104 let Ok(observed) = timeout_at(deadline, observe()).await else {
105 return Ok(WaitOutcome::TimedOut { last });
106 };
107 match (observed?, last) {
108 (Some(state), _) if finished(&state) => return Ok(WaitOutcome::Finished(state)),
109 (Some(state), _) => last = Some(state),
110 (None, Some(previous)) => return Ok(WaitOutcome::Vanished { last: previous }),
111 (None, None) => last = None,
112 }
113 sleep_until(deadline.min(Instant::now() + options.interval)).await;
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use std::{
120 cell::{Cell, RefCell},
121 collections::VecDeque,
122 };
123
124 use tokio::time::sleep;
125
126 use super::*;
127
128 fn options(timeout: u64, interval: u64) -> PollOptions {
129 PollOptions::new(Duration::from_millis(timeout)).interval(Duration::from_millis(interval))
130 }
131
132 struct Script {
134 steps: RefCell<VecDeque<(u64, Option<&'static str>)>>,
135 started: Cell<usize>,
136 }
137
138 impl Script {
139 fn new(steps: &[(u64, Option<&'static str>)]) -> Self {
140 Self {
141 steps: RefCell::new(steps.iter().copied().collect()),
142 started: Cell::new(0),
143 }
144 }
145
146 async fn observe(&self) -> Result<Option<&'static str>> {
147 self.started.set(self.started.get() + 1);
148 let (delay, state) = self.steps.borrow_mut().pop_front().unwrap_or((0, None));
149 sleep(Duration::from_millis(delay)).await;
150 Ok(state)
151 }
152 }
153
154 fn done(state: &&str) -> bool {
155 *state == "done"
156 }
157
158 #[tokio::test(start_paused = true)]
159 async fn a_stalled_observation_is_bounded_by_the_deadline() {
160 let script = Script::new(&[(0, Some("running")), (10_000, Some("done"))]);
161 let started = Instant::now();
162 let outcome = wait(options(100, 10), || script.observe(), done)
163 .await
164 .unwrap();
165 assert!(
166 matches!(
167 outcome,
168 WaitOutcome::TimedOut {
169 last: Some("running")
170 }
171 ),
172 "{outcome:?}"
173 );
174 assert_eq!(started.elapsed(), Duration::from_millis(100));
175 }
176
177 #[tokio::test(start_paused = true)]
178 async fn a_terminal_state_arriving_after_the_deadline_is_not_reported() {
179 let script = Script::new(&[(150, Some("done"))]);
180 let outcome = wait(options(10, 10), || script.observe(), done)
181 .await
182 .unwrap();
183 assert!(
184 matches!(outcome, WaitOutcome::TimedOut { last: None }),
185 "{outcome:?}"
186 );
187 }
188
189 #[tokio::test(start_paused = true)]
190 async fn no_observation_starts_at_or_after_the_deadline() {
191 let script = Script::new(&[(0, Some("running")); 10]);
192 let outcome = wait(options(100, 100), || script.observe(), done)
193 .await
194 .unwrap();
195 assert!(matches!(
196 outcome,
197 WaitOutcome::TimedOut {
198 last: Some("running")
199 }
200 ));
201 assert_eq!(script.started.get(), 1, "the sleep ends at the deadline");
202
203 let script = Script::new(&[(0, Some("done"))]);
204 let outcome = wait(options(0, 10), || script.observe(), done)
205 .await
206 .unwrap();
207 assert!(matches!(outcome, WaitOutcome::TimedOut { last: None }));
208 assert_eq!(script.started.get(), 0, "a zero timeout sends nothing");
209 }
210
211 #[tokio::test(start_paused = true)]
212 async fn a_state_that_disappears_after_being_seen_has_vanished() {
213 let script = Script::new(&[(0, None), (0, Some("running")), (0, None)]);
214 let outcome = wait(options(1_000, 10), || script.observe(), done)
215 .await
216 .unwrap();
217 assert!(
218 matches!(outcome, WaitOutcome::Vanished { last: "running" }),
219 "{outcome:?}"
220 );
221 assert_eq!(outcome.last(), Some(&"running"));
222 assert!(outcome.finished().is_none());
223 }
224
225 #[tokio::test(start_paused = true)]
226 async fn finishing_within_the_deadline_wins_and_errors_end_the_wait() {
227 let script = Script::new(&[(0, Some("running")), (90, Some("done"))]);
228 let outcome = wait(options(100, 10), || script.observe(), done)
229 .await
230 .unwrap();
231 assert_eq!(outcome.finished(), Some("done"));
232
233 let error = wait(
234 options(100, 10),
235 || async { Err::<Option<()>, _>(crate::Error::InvalidRequest("boom".into())) },
236 |()| false,
237 )
238 .await
239 .unwrap_err();
240 assert!(matches!(error, crate::Error::InvalidRequest(_)));
241 }
242
243 #[tokio::test(start_paused = true)]
244 async fn dropping_the_wait_cancels_the_running_observation() {
245 let script = Script::new(&[(10_000, Some("done"))]);
246 let wait = wait(options(60_000, 10), || script.observe(), done);
247 assert!(
248 tokio::time::timeout(Duration::from_millis(50), wait)
249 .await
250 .is_err()
251 );
252 assert_eq!(script.started.get(), 1);
253 }
254}