async_timers/
lib.rs

1//! Async timers crate
2//!
3//! This library provides timers that can be easily scheduled and canceled. For example, the tokio's [`tokio::time::Interval`] has no way of stopping the timer.
4//! You could have set the interval duration to a very big value, however that is rather a work around. Also, tokio's [`tokio::time::Sleep`] is a one-time use object,
5//! meaning it's .await requires to move the object and requires you to recreated it when you need to sleep again.
6//!
7//! This crate provides [`PeriodicTimer`] and [`OneshotTimer`] that aim to make the use of timers more pleasant.
8//! This timers have methods to cancel and restart timers.
9//!
10//! ## Usage
11//!
12//! ```
13//! use async_timers::{OneshotTimer, PeriodicTimer};
14//! use tokio::time::Duration;
15//!
16//! #[tokio::main]
17//! async fn main() {
18//!     let mut start_delay = OneshotTimer::expired();
19//!     let mut stop_delay = OneshotTimer::expired();
20//!     let mut periodic = PeriodicTimer::stopped();
21//!
22//!     let mut exit_loop_delay = OneshotTimer::scheduled(Duration::from_secs(10));
23//!
24//!     // The following call will block forever
25//!     // start_delay.tick().await;
26//!
27//!     // Useful in event loop in select! blocks
28//!
29//!     start_delay.schedule(Duration::from_secs(2));
30//!     println!("Periodic timer will start in 2 sec");
31//!
32//!     loop {
33//!         tokio::select! {
34//!             _ = start_delay.tick() => {
35//!                 // Start periodic timer with period of 500 ms
36//!                 periodic.start(Duration::from_millis(500));
37//!
38//!                 stop_delay.schedule(Duration::from_secs(3));
39//!                 println!("Periodic timer will stop in 3 sec");
40//!             }
41//!             _ = stop_delay.tick() => {
42//!                 // Stop periodic timer
43//!                 periodic.stop();
44//!                 exit_loop_delay.schedule(Duration::from_secs(3));
45//!                 println!("Periodic timer stopped. Will exit in 3 sec");
46//!             }
47//!             _ = periodic.tick() => {
48//!                 println!("Periodic tick!");
49//!             }
50//!             _ = exit_loop_delay.tick() => {
51//!                 println!("Bye!");
52//!                 break;
53//!             }
54//!         }
55//!     }
56//! }
57//! ```
58//!
59
60use std::task;
61
62use futures::Future;
63use tokio::time::{interval, sleep_until, Duration, Instant, Interval};
64
65/// NeverExpire is a future that never unblocks
66#[derive(Default, Debug)]
67struct NeverExpire {}
68
69impl Future for NeverExpire {
70    type Output = Instant;
71
72    fn poll(
73        self: std::pin::Pin<&mut Self>,
74        _cx: &mut std::task::Context<'_>,
75    ) -> std::task::Poll<Self::Output> {
76        task::Poll::Pending
77    }
78}
79
80/// PeriodicTimer expires on given interval
81///
82/// PeriodicTimer is an extension and built on top of [`tokio::time::Interval`].
83/// It can be in two states: [`PeriodicTimer::Started`] and [`PeriodicTimer::Stopped`].
84/// When in [`PeriodicTimer::Started`] state the timer will expire every interval duration but
85/// when in [`PeriodicTimer::Stopped`] it won't expire until the timer is started again.
86///
87/// ```
88/// use async_timers::PeriodicTimer;
89/// use tokio::time::{Duration, timeout};
90///
91/// #[tokio::main]
92/// async fn main() {
93///     let mut timer = PeriodicTimer::started(Duration::from_millis(10));
94///
95///     timer.tick().await;
96///     timer.tick().await;
97///     timer.tick().await;
98///
99///     // approximately 30ms have elapsed.
100///
101///     let result = timeout(Duration::from_millis(100), timer.tick()).await;
102///     assert!(result.is_ok(), "Timeout should not occur since timer is running");
103///
104///     timer.stop();
105///
106///     let result = timeout(Duration::from_millis(100), timer.tick()).await;
107///     assert!(result.is_err(), "Timeout should occur since timer is stopped");
108/// }
109/// ```
110#[derive(Default, Debug)]
111pub enum PeriodicTimer {
112    Started(Interval),
113    #[default]
114    Stopped,
115}
116
117impl PeriodicTimer {
118    /// Create started timer with the given `period`
119    pub fn started(period: Duration) -> Self {
120        Self::Started(interval(period))
121    }
122
123    /// Create stopped timer
124    pub fn stopped() -> Self {
125        Self::Stopped
126    }
127
128    /// Start the timer with given `period`
129    pub fn start(&mut self, period: Duration) {
130        *self = Self::started(period);
131    }
132
133    /// Stop the timer
134    pub fn stop(&mut self) {
135        *self = Self::stopped()
136    }
137
138    /// Returns a [`Future`] that will expire based on timer's state
139    pub async fn tick(&mut self) -> Instant {
140        match self {
141            Self::Started(interval) => interval.tick().await,
142            Self::Stopped => NeverExpire::default().await,
143        }
144    }
145}
146
147/// OneshotTimer expires once after a given duration
148///
149/// OneshotTimer is used for tasks that need to be executed once after some delay.
150/// OneshotTimer is an extension and built on top of [`tokio::time::Sleep`].
151/// In [`OneshotTimer::Scheduled`] state it will expire *once* and transition into
152/// [`OneshotTimer::Expired`] state.
153///
154/// ```
155/// use async_timers::OneshotTimer;
156/// use tokio::time::{Duration, timeout};
157///
158/// #[tokio::main]
159/// async fn main() {
160///     let mut timer = OneshotTimer::scheduled(Duration::from_millis(10));
161///
162///     timer.tick().await;
163///
164///     // approximately 10ms have elapsed.
165///
166///     let result = timeout(Duration::from_millis(100), timer.tick()).await;
167///     assert!(result.is_err(), "Timeout should occur since timer is expired");
168///
169///     timer.schedule(Duration::from_millis(30));
170///
171///     let result = timeout(Duration::from_millis(100), timer.tick()).await;
172///     assert!(result.is_ok(), "Timeout should not occur since timer has been scheduled");
173/// }
174/// ```
175#[derive(Default, Debug)]
176pub enum OneshotTimer {
177    Scheduled(Instant),
178    #[default]
179    Expired,
180}
181
182impl OneshotTimer {
183    /// Create a timer scheduled to be expired after `duration`
184    pub fn scheduled(duration: Duration) -> Self {
185        Self::Scheduled(Instant::now() + duration)
186    }
187
188    /// Create a timer that won't expire
189    pub fn expired() -> Self {
190        Self::Expired
191    }
192
193    /// Schedule a new duration
194    pub fn schedule(&mut self, duration: Duration) {
195        *self = Self::scheduled(duration);
196    }
197
198    /// Cancel the timer
199    pub fn cancel(&mut self) {
200        *self = Self::expired()
201    }
202
203    /// Returns a [`Future`] that will expire based on timer's state
204    pub async fn tick(&mut self) {
205        match self {
206            Self::Scheduled(instant) => {
207                sleep_until(*instant).await;
208                *self = Self::expired();
209            }
210            Self::Expired => {
211                NeverExpire::default().await;
212            }
213        }
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[tokio::test]
222    async fn test_periodic_timer() {
223        let mut timer1 = PeriodicTimer::stopped();
224        let mut timer2 = PeriodicTimer::started(Duration::from_secs(2));
225
226        let mut timer1_expired = false;
227        let mut timer2_expired = false;
228
229        tokio::select! {
230            _ = timer1.tick() => {
231                timer1_expired = true;
232            }
233            _ = timer2.tick() => {
234                timer2_expired = true;
235            }
236        }
237
238        assert!(!timer1_expired, "timer1 should not have expired");
239        assert!(timer2_expired, "timer1 should have expired");
240
241        timer1.start(Duration::from_secs(1));
242        timer2.stop();
243
244        timer1_expired = false;
245        timer2_expired = false;
246
247        tokio::select! {
248            _ = timer1.tick() => {
249                timer1_expired = true;
250            }
251            _ = timer2.tick() => {
252                timer2_expired = true;
253            }
254        }
255
256        assert!(timer1_expired, "timer1 should have expired");
257        assert!(!timer2_expired, "timer2 should not have expired");
258    }
259
260    #[tokio::test]
261    async fn test_oneshot_timer() {
262        let mut timer1 = OneshotTimer::expired();
263        let mut timer2 = OneshotTimer::scheduled(Duration::from_secs(2));
264
265        let mut timer1_expired = false;
266        let mut timer2_expired = false;
267
268        tokio::select! {
269            _ = timer1.tick() => {
270                timer1_expired = true;
271            }
272            _ = timer2.tick() => {
273                timer2_expired = true;
274            }
275        }
276
277        assert!(!timer1_expired, "timer1 should not have expired");
278        assert!(timer2_expired, "timer1 should have expired");
279
280        timer1.schedule(Duration::from_secs(1));
281
282        timer1_expired = false;
283        timer2_expired = false;
284
285        tokio::select! {
286            _ = timer1.tick() => {
287                timer1_expired = true;
288            }
289            _ = timer2.tick() => {
290                timer2_expired = true;
291            }
292        }
293
294        assert!(timer1_expired, "timer1 should have expired");
295        assert!(!timer2_expired, "timer2 should not have expired");
296
297        timer1.schedule(Duration::from_secs(1));
298        timer2.schedule(Duration::from_secs(2));
299
300        timer1.cancel();
301
302        timer1_expired = false;
303        timer2_expired = false;
304
305        tokio::select! {
306            _ = timer1.tick() => {
307                timer1_expired = true;
308            }
309            _ = timer2.tick() => {
310                timer2_expired = true;
311            }
312        }
313
314        assert!(!timer1_expired, "timer1 should not have expired");
315        assert!(timer2_expired, "timer2 should have expired");
316    }
317
318    #[tokio::test]
319    async fn test_oneshot_state() {
320        let mut timer1 = OneshotTimer::scheduled(Duration::from_secs(1));
321        let result = tokio::time::timeout(Duration::from_millis(1500), timer1.tick()).await;
322        assert!(result.is_ok(), "Should not timeout");
323
324        let mut timer1 = OneshotTimer::scheduled(Duration::from_secs(5));
325        let mut timer2 = OneshotTimer::scheduled(Duration::from_secs(2));
326
327        tokio::select! {
328            _ = timer1.tick() => {}
329            _ = timer2.tick() => {}
330        }
331
332        match timer1 {
333            OneshotTimer::Scheduled(_) => {}
334            OneshotTimer::Expired => assert!(false, "Should be in scheduled state"),
335        }
336
337        let result = tokio::time::timeout(Duration::from_millis(3500), timer1.tick()).await;
338        assert!(result.is_ok(), "Should not timeout");
339
340        match timer1 {
341            OneshotTimer::Scheduled(_) => assert!(false, "Timer should be in expired state"),
342            OneshotTimer::Expired => {}
343        }
344    }
345
346    #[tokio::test]
347    async fn test_my_task() {
348        struct MyTask {
349            period: PeriodicTimer,
350        }
351
352        impl MyTask {
353            fn new() -> Self {
354                Self {
355                    period: PeriodicTimer::started(Duration::from_secs(1)),
356                }
357            }
358
359            fn do_work(&mut self) {}
360        }
361
362        let mut task = MyTask::new();
363        let mut sleep = OneshotTimer::scheduled(Duration::from_secs(3));
364
365        let result = tokio::time::timeout(Duration::from_secs(10), async move {
366            for _ in 0..3 {
367                tokio::select! {
368                    _ = task.period.tick() => {
369                        task.do_work();
370                        task.period.stop();
371                    }
372                    _ = sleep.tick() => {
373                        task.period.start(Duration::from_secs(1));
374                    }
375                }
376            }
377        })
378        .await;
379
380        assert!(result.is_ok(), "Should not timeout");
381    }
382}