nextest_runner/time/pausable_sleep.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
// Copyright (c) The nextest Contributors
// SPDX-License-Identifier: MIT OR Apache-2.0
use pin_project_lite::pin_project;
use std::{future::Future, pin::Pin, task::Poll, time::Duration};
use tokio::time::{Instant, Sleep};
pub(crate) fn pausable_sleep(duration: Duration) -> PausableSleep {
PausableSleep::new(duration)
}
pin_project! {
/// A wrapper around `tokio::time::Sleep` that can also be paused, resumed and reset.
#[derive(Debug)]
pub(crate) struct PausableSleep {
#[pin]
sleep: Sleep,
duration: Duration,
pause_state: SleepPauseState,
}
}
impl PausableSleep {
fn new(duration: Duration) -> Self {
Self {
sleep: tokio::time::sleep(duration),
duration,
pause_state: SleepPauseState::Running,
}
}
#[allow(dead_code)]
pub(crate) fn is_paused(&self) -> bool {
matches!(self.pause_state, SleepPauseState::Paused { .. })
}
pub(crate) fn pause(self: Pin<&mut Self>) {
let this = self.project();
match &*this.pause_state {
SleepPauseState::Running => {
// Figure out how long there is until the deadline.
let deadline = this.sleep.deadline();
this.sleep.reset(far_future());
// This will return 0 if the deadline has passed. That's fine because we'll just
// reset the timer back to 0 in resume, which will behave correctly.
let remaining = deadline.duration_since(Instant::now());
*this.pause_state = SleepPauseState::Paused { remaining };
}
SleepPauseState::Paused { remaining } => {
panic!("illegal state transition: pause() called while sleep was paused (remaining = {remaining:?})");
}
}
}
pub(crate) fn resume(self: Pin<&mut Self>) {
let this = self.project();
match &*this.pause_state {
SleepPauseState::Paused { remaining } => {
this.sleep.reset(Instant::now() + *remaining);
*this.pause_state = SleepPauseState::Running;
}
SleepPauseState::Running => {
panic!("illegal state transition: resume() called while sleep was running");
}
}
}
/// Resets the sleep to the given duration.
///
/// * If the timer is currently running, it will be reset to
/// `Instant::now()` plus the last duration provided via
/// [`pausable_sleep`] or [`Self::reset`].
///
/// * If it is currently paused, it will be reset to the new duration
/// whenever it is resumed.
pub(crate) fn reset(self: Pin<&mut Self>, duration: Duration) {
let this = self.project();
*this.duration = duration;
match this.pause_state {
SleepPauseState::Running => {
this.sleep.reset(Instant::now() + duration);
}
SleepPauseState::Paused { remaining } => {
*remaining = duration;
}
}
}
/// Resets the sleep to the last duration provided.
///
/// * If the timer is currently running, it will be reset to
/// `Instant::now()` plus the last duration provided via
/// [`pausable_sleep`] or [`Self::reset`].
///
/// * If it is currently paused, it will be reset to the new duration
/// whenever it is resumed.
pub(crate) fn reset_last_duration(self: Pin<&mut Self>) {
let duration = self.duration;
self.reset(duration);
}
}
impl Future for PausableSleep {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
let this = self.project();
// Always call into this.sleep.
//
// We don't do anything special for paused sleeps here. That's because
// on pause, the sleep is reset to a far future deadline. Calling poll
// will mean that the future gets registered with the time driver (so is
// not going to be stuck without a waker, even though the waker will
// never end up waking the task in practice).
this.sleep.poll(cx)
}
}
#[derive(Debug, PartialEq, Eq)]
enum SleepPauseState {
Running,
Paused { remaining: Duration },
}
// Cribbed from tokio.
fn far_future() -> Instant {
// Roughly 30 years from now.
// API does not provide a way to obtain max `Instant`
// or convert specific date in the future to instant.
// 1000 years overflows on macOS, 100 years overflows on FreeBSD.
Instant::now() + Duration::from_secs(86400 * 365 * 30)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn reset_on_sleep() {
const TICK: Duration = Duration::from_millis(500);
// Create a very short timer.
let mut sleep = std::pin::pin!(pausable_sleep(Duration::from_millis(1)));
// Pause the timer.
sleep.as_mut().pause();
assert!(
!sleep.as_mut().sleep.is_elapsed(),
"underlying sleep has been suspended"
);
// Now set the timer to one tick. This should *not* cause the timer to
// be reset -- instead, the new timer should be buffered until the timer
// is resumed.
sleep.as_mut().reset(TICK);
assert_eq!(
sleep.as_ref().pause_state,
SleepPauseState::Paused { remaining: TICK }
);
assert!(
!sleep.as_mut().sleep.is_elapsed(),
"underlying sleep is still suspended"
);
// Now sleep for 2 ticks. The timer should still be paused and not
// completed.
tokio::time::sleep(2 * TICK).await;
assert!(
!sleep.as_mut().sleep.is_elapsed(),
"underlying sleep is still suspended after waiting 2 ticks"
);
// Now resume the timer and wait for it to complete. It should take
// around 1 tick starting from this point.
let now = Instant::now();
sleep.as_mut().resume();
sleep.as_mut().await;
assert!(
sleep.as_mut().sleep.is_elapsed(),
"underlying sleep has finally elapsed"
);
assert!(now.elapsed() >= TICK);
}
}