Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tokio/interval: Fix overflow for interval tick #7193

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions tokio/src/time/interval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,12 @@ impl MissedTickBehavior {
/// If a tick is missed, this method is called to determine when the next tick should happen.
fn next_timeout(&self, timeout: Instant, now: Instant, period: Duration) -> Instant {
match self {
Self::Burst => timeout + period,
Self::Delay => now + period,
Self::Burst => timeout
.checked_add(period)
.unwrap_or_else(Instant::far_future),
Self::Delay => now.checked_add(period).unwrap_or_else(Instant::far_future),
Self::Skip => {
now + period
now.checked_add(period).unwrap_or_else(Instant::far_future)
- Duration::from_nanos(
((now - timeout).as_nanos() % period.as_nanos())
.try_into()
Expand Down Expand Up @@ -475,7 +477,8 @@ impl Interval {
// However, if a tick took excessively long and we are now behind,
// schedule the next tick according to how the user specified with
// `MissedTickBehavior`
let next = if now > timeout + Duration::from_millis(5) {

let next = if now.saturating_duration_since(timeout) > Duration::from_millis(5) {
self.missed_tick_behavior
.next_timeout(timeout, now, self.period)
} else {
Expand Down
21 changes: 19 additions & 2 deletions tokio/tests/time_interval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,8 +470,25 @@ async fn stream_with_interval_poll_tick_no_waking() {
assert_eq!(items, vec![]);
}

#[tokio::test(start_paused = true)]
#[tokio::test]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't this test use start_paused?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a bit of delay between two subsequent poll_tick calls (5ms) to enter the misstick behavior and trigger the overflow panic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, a test that takes 2 seconds to complete is not acceptable. I'm quite certain that the test works just fine with start_paused - the parameter should not change the behavior of tests that rely on time other than making them run faster.

async fn interval_doesnt_panic_max_duration_when_polling() {
let mut timer = task::spawn(time::interval(Duration::MAX));
let mut timer = task::spawn(time::interval(Duration::from_secs(u64::MAX)));

// The `Interval::delay` is not ready yet.
let result = timer.enter(|cx, mut timer| timer.poll_tick(cx));
assert!(result.is_pending());

// Sleep 1 second to ensure we capture a missed tick.
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;

// Call the poll tick again to check the duration doesn't panic.
// These 2 polls are equivalent to `timer.tick().await` that disarm the first interval fire.
assert_ready!(timer.enter(|cx, mut timer| timer.poll_tick(cx)));

// Sleep again.
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;

// This time we expect the timer to fire at the specified interval (ie too far in the future).
let result = timer.enter(|cx, mut timer| timer.poll_tick(cx));
assert!(result.is_pending());
}