Skip to content

Commit

Permalink
Do not panic on timeout(Duration::MAX)
Browse files Browse the repository at this point in the history
It is tempting to use very large `Duration` value to get a practically
infinite timeout.

Before this commit tokio panics on checked Instant + Duration
overflow.

This commit implements very simple fix: if Instant + Duration
overflows, we use duration = 100 years. Better fix should avoid
firing a timer on duration overflow. It requires deeper understanding
how timers work, but also it is not clear, for example, what
`Sleep::deadline` function should return.

Similar fix is done for `sleep`.
  • Loading branch information
stepancheg committed Feb 24, 2021
1 parent c9d2a36 commit b050097
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 2 deletions.
9 changes: 8 additions & 1 deletion tokio/src/time/driver/sleep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ pub fn sleep_until(deadline: Instant) -> Sleep {
// Alias for old name in 0.x
#[cfg_attr(docsrs, doc(alias = "delay_for"))]
pub fn sleep(duration: Duration) -> Sleep {
sleep_until(Instant::now() + duration)
match Instant::now().checked_add(duration) {
Some(deadline) => sleep_until(deadline),
None => sleep_until(Instant::far_future()),
}
}

pin_project! {
Expand Down Expand Up @@ -168,6 +171,10 @@ impl Sleep {
Sleep { deadline, entry }
}

pub(crate) fn far_future() -> Sleep {
Self::new_timeout(Instant::far_future())
}

/// Returns the instant at which the future will complete.
pub fn deadline(&self) -> Instant {
self.deadline
Expand Down
7 changes: 7 additions & 0 deletions tokio/src/time/instant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ impl Instant {
Instant { std }
}

pub(crate) fn far_future() -> Instant {
// Roughly 100 years from now.
// API does not provide a way to obtain max `Instant`
// or convert specific date in the future to instant.
Self::now() + Duration::from_secs(86400 * 365 * 100)
}

/// Convert the value into a `std::time::Instant`.
pub fn into_std(self) -> std::time::Instant {
self.std
Expand Down
6 changes: 5 additions & 1 deletion tokio/src/time/timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ pub fn timeout<T>(duration: Duration, future: T) -> Timeout<T>
where
T: Future,
{
let delay = Sleep::new_timeout(Instant::now() + duration);
let deadline = Instant::now().checked_add(duration);
let delay = match deadline {
Some(deadline) => Sleep::new_timeout(deadline),
None => Sleep::far_future(),
};
Timeout::new_with_delay(future, delay)
}

Expand Down
27 changes: 27 additions & 0 deletions tokio/tests/time_timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,33 @@ async fn future_and_timeout_in_future() {
assert_ready_ok!(fut.poll()).unwrap();
}

#[tokio::test]
async fn very_large_timeout() {
time::pause();

// Not yet complete
let (tx, rx) = oneshot::channel();

// copy-paste unstable `Duration::MAX`
let duration_max = Duration::from_secs(u64::MAX) + Duration::from_nanos(999_999_999);

// Wrap it with a deadline
let mut fut = task::spawn(timeout(duration_max, rx));

// Ready!
assert_pending!(fut.poll());

// Turn the timer, it runs for the elapsed time
time::advance(Duration::from_secs(86400 * 365 * 90)).await;

assert_pending!(fut.poll());

// Complete the future
tx.send(()).unwrap();

assert_ready_ok!(fut.poll()).unwrap();
}

#[tokio::test]
async fn deadline_now_elapses() {
use futures::future::pending;
Expand Down

0 comments on commit b050097

Please sign in to comment.