moduvex_runtime/time/
interval.rs1use std::future::Future;
8use std::pin::Pin;
9use std::task::{Context, Poll};
10use std::time::{Duration, Instant};
11
12use super::{with_timer_wheel, TimerId};
13
14pub struct Interval {
16 period: Duration,
18 next_deadline: Instant,
20 missed: u64,
22}
23
24impl Interval {
25 pub(crate) fn new(period: Duration) -> Self {
26 assert!(!period.is_zero(), "interval period must be non-zero");
27 Self {
28 period,
29 next_deadline: Instant::now() + period,
30 missed: 0,
31 }
32 }
33
34 pub fn tick(&mut self) -> TickFuture<'_> {
39 TickFuture {
40 interval: self,
41 timer_id: None,
42 }
43 }
44}
45
46pub struct TickFuture<'a> {
48 interval: &'a mut Interval,
49 timer_id: Option<TimerId>,
50}
51
52impl<'a> Future for TickFuture<'a> {
53 type Output = Instant;
54
55 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
56 let now = Instant::now();
57
58 if now >= self.interval.next_deadline {
60 if let Some(id) = self.timer_id.take() {
62 with_timer_wheel(|w| {
63 w.cancel(id);
64 });
65 }
66
67 let fired_at = self.interval.next_deadline;
68
69 let elapsed = now.duration_since(fired_at);
71 let extra_ticks = (elapsed.as_nanos() / self.interval.period.as_nanos()) as u64;
72 self.interval.missed += extra_ticks;
73 self.interval.next_deadline =
74 fired_at + self.interval.period * (extra_ticks as u32 + 1);
75
76 return Poll::Ready(fired_at);
77 }
78
79 if let Some(old_id) = self.timer_id.take() {
81 with_timer_wheel(|w| {
82 w.cancel(old_id);
83 });
84 }
85 let deadline = self.interval.next_deadline;
86 let id = with_timer_wheel(|w| w.insert(deadline, cx.waker().clone()));
87 self.timer_id = Some(id);
88
89 Poll::Pending
90 }
91}
92
93impl<'a> Drop for TickFuture<'a> {
94 fn drop(&mut self) {
95 if let Some(id) = self.timer_id.take() {
96 with_timer_wheel(|w| {
97 w.cancel(id);
98 });
99 }
100 }
101}
102
103pub fn interval(period: Duration) -> Interval {
124 Interval::new(period)
125}
126
127#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::executor::block_on_with_spawn;
133
134 #[test]
135 fn interval_fires_multiple_times() {
136 block_on_with_spawn(async {
137 let mut ticker = interval(Duration::from_millis(50));
138 let before = Instant::now();
139
140 ticker.tick().await;
141 ticker.tick().await;
142 ticker.tick().await;
143
144 let elapsed = before.elapsed();
145 assert!(
147 elapsed >= Duration::from_millis(120),
148 "interval fired too fast: {:?}",
149 elapsed
150 );
151 assert!(
152 elapsed < Duration::from_millis(1000),
153 "interval took too long: {:?}",
154 elapsed
155 );
156 });
157 }
158
159 #[test]
160 #[should_panic(expected = "non-zero")]
161 fn interval_zero_period_panics() {
162 let _ = interval(Duration::ZERO);
163 }
164
165 #[test]
166 fn interval_tracks_missed_ticks() {
167 let period = Duration::from_millis(20);
170 let mut ticker = interval(period);
171
172 let wait_until = Instant::now() + period * 3;
174 while Instant::now() < wait_until {
175 std::hint::spin_loop();
176 }
177
178 block_on_with_spawn(async move {
180 let now = Instant::now();
181 ticker.tick().await;
182 let elapsed = now.elapsed();
183 assert!(
185 elapsed < Duration::from_millis(50),
186 "missed tick must resolve immediately, took {:?}",
187 elapsed
188 );
189 });
190 }
191}