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}