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);
    }
}