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::time::Interval::poll_tick(...) scheduling a wake-up even when returning Poll::Ready(...)? #5551

Closed
sgasse opened this issue Mar 16, 2023 · 3 comments · Fixed by #5553
Labels
A-tokio Area: The main tokio crate C-bug Category: This is a bug. E-help-wanted Call for participation: Help is requested to fix this issue. E-medium Call for participation: Experience needed to fix: Medium / intermediate M-time Module: tokio/time

Comments

@sgasse
Copy link
Contributor

sgasse commented Mar 16, 2023

Version

└── tokio v1.26.0
    └── tokio-macros v1.8.2 (proc-macro)

Platform
Linux 5.14

Description

Reading the documentation of Interval::poll_tick(...), it only states that a call to poll_tick(...) will schedule the task to receive a wake-up when the call returns Poll::Pending. I would understand this implicitly as "The task will not be scheduled for wake-up when we return Poll::Ready(...)". However it looks like that the task is scheduled to be polled again even when returning Poll::Ready(...). I noticed this today when building something with an interval.

While you could argue that this behavior makes sense for a periodic interval, I want to make sure that it is expected, otherwise something built on top of this might break some day. If it is expected, I am happy to submit a PR to update the documentation accordingly.

Below is an example to reproduce the behavior.

Cargo.toml:

[package]
name = "interval_ex"
version = "0.1.0"
edition = "2021"

[dependencies]
futures = "0.3.27"
tokio = { version = "1.26.0", features = [
    "macros",
    "time",
    "rt-multi-thread",
] }
use std::{
    pin::Pin,
    task::{Context, Poll},
    time::Instant,
};

use futures::{pin_mut, Stream, StreamExt};
use tokio::time::Interval;

struct IntervalStreamer {
    start: Instant,
    counter: u32,
    timer: Interval,
}

impl Stream for IntervalStreamer {
    type Item = u32;

    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
        let this = Pin::into_inner(self);
        match this.timer.poll_tick(cx) {
            Poll::Pending => {
                println!(
                    "Timer returned Poll::Pending after {:?}",
                    this.start.elapsed()
                );
                Poll::Pending
            }
            Poll::Ready(_) => {
                println!(
                    "Timer returned Poll::Ready after {:?}",
                    this.start.elapsed()
                );
                this.counter += 1;
                if this.counter % 4 == 0 {
                    Poll::Ready(Some(this.counter))
                } else {
                    // If `Interval::poll_tick(cx)` only schedules a wake-up
                    // when returning `Poll::Pending` and not on
                    // `Poll::Ready(...)`, this should stall the stream.
                    // But the stream continues.
                    Poll::Pending
                }
            }
        }
    }
}

#[tokio::main]
async fn main() {
    println!("Entering main...");
    let stream = IntervalStreamer {
        start: Instant::now(),
        counter: 0,
        timer: tokio::time::interval(std::time::Duration::from_secs(1)),
    };

    pin_mut!(stream);

    while let Some(_) = stream.next().await {
        println!("Stream returned an element");
    }
}

The output looks like this:

Entering main...
Timer returned Poll::Pending after 10.49µs
Timer returned Poll::Ready after 1.121431ms
Timer returned Poll::Ready after 1.001399627s
Timer returned Poll::Ready after 2.000677712s
Timer returned Poll::Ready after 3.000994507s
Stream returned an element
Timer returned Poll::Pending after 3.001008401s
Timer returned Poll::Ready after 4.00128182s
Timer returned Poll::Ready after 5.000681116s
Timer returned Poll::Ready after 6.00100176s
Timer returned Poll::Ready after 7.00124704s
Stream returned an element
Timer returned Poll::Pending after 7.001259485s
Timer returned Poll::Ready after 8.001502115s
Timer returned Poll::Ready after 9.000941194s
Timer returned Poll::Ready after 10.001269224s
Timer returned Poll::Ready after 11.001546952s
Stream returned an element
Timer returned Poll::Pending after 11.001559985s
Timer returned Poll::Ready after 12.000852821s
Timer returned Poll::Ready after 13.001271725s
Timer returned Poll::Ready after 14.001628621s
Timer returned Poll::Ready after 15.001000159s
Stream returned an element
...

If Interval::poll_tick(cx) did not schedule the task for being polled again when returning Poll::Ready(...), I would expect the stream to stall. Instead, it yields item as if someone scheduled for a wake-up.

@sgasse sgasse added A-tokio Area: The main tokio crate C-bug Category: This is a bug. labels Mar 16, 2023
@Darksonn Darksonn added M-time Module: tokio/time E-help-wanted Call for participation: Help is requested to fix this issue. E-medium Call for participation: Experience needed to fix: Medium / intermediate labels Mar 16, 2023
@Darksonn
Copy link
Contributor

I agree that we shouldn't schedule a wakeup in that case. If somebody wants to submit a PR for this, then please go ahead.

@sgasse
Copy link
Contributor Author

sgasse commented Mar 17, 2023

Alright thank you for confirming, I am on it 🙂

sgasse added a commit to sgasse/tokio that referenced this issue Mar 17, 2023
When `tokio::time::Interval::poll_tick()` returns `Poll::Pending`, it
schedules itself for being woken up again through the waker of the
passed context, which is correct behavior.

However when `Poll::Ready(_)` is returned, the interval timer should be
reset but not scheduled to be woken up again as this is up to the
caller.
sgasse added a commit to sgasse/tokio that referenced this issue Mar 17, 2023
When `tokio::time::Interval::poll_tick()` returns `Poll::Pending`, it
schedules itself for being woken up again through the waker of the
passed context, which is correct behavior.

However when `Poll::Ready(_)` is returned, the interval timer should be
reset but not scheduled to be woken up again as this is up to the
caller.

This commit fixes the bug by introducing a `reset_without_reregister`
method on `TimerEntry` which is called by `Intervall::poll_tick(cx)` in
case the delay poll returns `Poll::Ready(_)`.
@sgasse
Copy link
Contributor Author

sgasse commented Mar 17, 2023

@Darksonn I have only used tokio as a user but I think I found a way to fix the bug. Could you take a look at the proposed solution? If you like it, I am happy to write more concise and short tests and polish it up 🙂

I am also wondering - could this change break some users which unwittingly relied on this behavior? 🤔

sgasse added a commit to sgasse/tokio that referenced this issue Mar 17, 2023
When `tokio::time::Interval::poll_tick()` returns `Poll::Pending`, it
schedules itself for being woken up again through the waker of the
passed context, which is correct behavior.

However when `Poll::Ready(_)` is returned, the interval timer should be
reset but not scheduled to be woken up again as this is up to the
caller.

This commit fixes the bug by introducing a `reset_without_reregister`
method on `TimerEntry` which is called by `Intervall::poll_tick(cx)` in
case the delay poll returns `Poll::Ready(_)`.
sgasse added a commit to sgasse/tokio that referenced this issue Mar 17, 2023
When `tokio::time::Interval::poll_tick()` returns `Poll::Pending`, it
schedules itself for being woken up again through the waker of the
passed context, which is correct behavior.

However when `Poll::Ready(_)` is returned, the interval timer should be
reset but not scheduled to be woken up again as this is up to the
caller.

This commit fixes the bug by introducing a `reset_without_reregister`
method on `TimerEntry` which is called by `Intervall::poll_tick(cx)` in
case the delay poll returns `Poll::Ready(_)`.
sgasse added a commit to sgasse/tokio that referenced this issue Mar 17, 2023
When `tokio::time::Interval::poll_tick()` returns `Poll::Pending`, it
schedules itself for being woken up again through the waker of the
passed context, which is correct behavior.

However when `Poll::Ready(_)` is returned, the interval timer should be
reset but not scheduled to be woken up again as this is up to the
caller.

This commit fixes the bug by introducing a `reset_without_reregister`
method on `TimerEntry` which is called by `Intervall::poll_tick(cx)` in
case the delay poll returns `Poll::Ready(_)`.
sgasse added a commit to sgasse/tokio that referenced this issue Mar 17, 2023
When `tokio::time::Interval::poll_tick()` returns `Poll::Pending`, it
schedules itself for being woken up again through the waker of the
passed context, which is correct behavior.

However when `Poll::Ready(_)` is returned, the interval timer should be
reset but not scheduled to be woken up again as this is up to the
caller.

This commit fixes the bug by introducing a `reset_without_reregister`
method on `TimerEntry` which is called by `Intervall::poll_tick(cx)` in
case the delay poll returns `Poll::Ready(_)`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate C-bug Category: This is a bug. E-help-wanted Call for participation: Help is requested to fix this issue. E-medium Call for participation: Experience needed to fix: Medium / intermediate M-time Module: tokio/time
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants