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

time: fix wake-up with interval on Ready #5553

Merged

Conversation

sgasse
Copy link
Contributor

@sgasse sgasse commented Mar 17, 2023

Motivation

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.

Currently, resetting the timer always involves reregistering the last waker which polled the internal Sleep to be woken up on completion.

Solution

This PR 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(_).

Closes: #5551

@github-actions github-actions bot added the R-loom Run loom tests on this PR label Mar 17, 2023
@sgasse sgasse force-pushed the fix/interval_do_not_reregister_on_ready branch from c48f061 to b2d67ef Compare March 17, 2023 10:13
@Darksonn Darksonn changed the title time: fix wake-up with interval on Ready (#5551) time: fix wake-up with interval on Ready Mar 17, 2023
@Darksonn Darksonn added A-tokio Area: The main tokio crate M-time Module: tokio/time labels Mar 17, 2023
Copy link
Contributor

@Darksonn Darksonn left a comment

Choose a reason for hiding this comment

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

Thank you so much for the PR!

The implementation looks ok to me, but I've asked @carllerche to take a look at whether there's anything special to think about when skipping reregister.

I have some comments regarding your test.

@@ -209,3 +209,76 @@ fn poll_next(interval: &mut task::Spawn<time::Interval>) -> Poll<Instant> {
fn ms(n: u64) -> Duration {
Duration::from_millis(n)
}

mod tmp_tests {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the module?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initially I intended to keep the specific helper struct with the Stream implementation and the test together, but I guess it's not needed. I removed it.

Comment on lines 214 to 218
use std::{
pin::Pin,
task::{Context, Poll},
time::Instant,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't use this import style in Tokio.

Suggested change
use std::{
pin::Pin,
task::{Context, Poll},
time::Instant,
};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Instant;

Copy link
Member

Choose a reason for hiding this comment

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

Yes, we don't currently use it.

As an aside, I wish we did and would take a PR that did a big pass-through of Tokio to update all imports... though others should chime in before anyone does the work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah OK thanks for letting me know - updated.

timer: crate::time::interval(std::time::Duration::from_millis(10)),
};

pin_mut!(stream);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pin_mut!(stream);
tokio::pin!(stream);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}
}

#[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.

This should make the test execute much faster by having it instantly skip forward to the next timer whenever a sleep happens.

Suggested change
#[tokio::test]
#[tokio::test(start_paused = true)]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK nice! With this, do I need to manually use any tokio::time::advance, or will it automatically skip forward?

Comment on lines 256 to 258
// Schedule this task for wake-up
cx.waker().wake_by_ref();
Poll::Pending
Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I can tell, you're not actually testing whether it emits a wakeup or not. This test would pass either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the test was not complete but more a manual thing until I hear back on the approach. Sorry that I did not explain that better. I added two tests now which tease out the difference.

@@ -543,6 +543,17 @@ impl TimerEntry {
}
}

pub(crate) fn reset_without_reregister(mut self: Pin<&mut Self>, new_time: Instant) {
Copy link
Member

Choose a reason for hiding this comment

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

This function is very similar to reset. Maybe reset should take an argument reregister: bool. I'm not 100% sure, but it is worth considering given the unsafe in the fn.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah thanks, good point. I checked and there were not too many usages of TimerEntry::reset to update, so I added the boolean flag and removed reset_without_reregister.

/// To call this method, you will usually combine the call with
/// [`Pin::as_mut`], which lets you call the method without consuming the
/// `Sleep` itself.
pub fn reset_without_reregister(self: Pin<&mut Self>, deadline: Instant) {
Copy link
Member

Choose a reason for hiding this comment

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

Adding a new public function should be a high bar. Either the existing behavior is incorrect, and we should fix it or accept it the way it is and evaluate adding a new API.

My concern with reset_without_reregister is that it makes the caller decide. Which variant should they use? Choosing one requires knowing about "waking," which isn't required for using Tokio as a beginner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm that is a good point 🤔 I do think that users of tokio::time::Sleep would always want the re-registering variant. The non-re-registering is only needed for the specific use case in tokio::time::Interval. I changed the visibility to pub(crate). Would that be OK?

Comment on lines 214 to 218
use std::{
pin::Pin,
task::{Context, Poll},
time::Instant,
};
Copy link
Member

Choose a reason for hiding this comment

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

Yes, we don't currently use it.

As an aside, I wish we did and would take a PR that did a big pass-through of Tokio to update all imports... though others should chime in before anyone does the work.

@sgasse sgasse force-pushed the fix/interval_do_not_reregister_on_ready branch 2 times, most recently from 82b9a7f to c2586d6 Compare March 17, 2023 17:13
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 sgasse force-pushed the fix/interval_do_not_reregister_on_ready branch from c2586d6 to 8dffc8d Compare March 17, 2023 17:18
@sgasse
Copy link
Contributor Author

sgasse commented Mar 27, 2023

@Darksonn or @carllerche would you have time to check the updated version? 🙂

Copy link
Contributor

@Darksonn Darksonn left a comment

Choose a reason for hiding this comment

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

Thanks.

@Darksonn Darksonn merged commit 68b02db into tokio-rs:master Mar 27, 2023
crapStone pushed a commit to Calciumdibromid/CaBr2 that referenced this pull request Mar 29, 2023
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tokio](https://tokio.rs) ([source](https://github.com/tokio-rs/tokio)) | dependencies | minor | `1.26.0` -> `1.27.0` |
| [tokio](https://tokio.rs) ([source](https://github.com/tokio-rs/tokio)) | dev-dependencies | minor | `1.26.0` -> `1.27.0` |

---

### Release Notes

<details>
<summary>tokio-rs/tokio</summary>

### [`v1.27.0`](https://github.com/tokio-rs/tokio/releases/tag/tokio-1.27.0): Tokio v1.27.0

[Compare Source](tokio-rs/tokio@tokio-1.26.0...tokio-1.27.0)

##### 1.27.0 (March 27th, 2023)

This release bumps the MSRV of Tokio to 1.56. ([#&#8203;5559])

##### Added

-   io: add `async_io` helper method to sockets ([#&#8203;5512])
-   io: add implementations of `AsFd`/`AsHandle`/`AsSocket` ([#&#8203;5514], [#&#8203;5540])
-   net: add `UdpSocket::peek_sender()` ([#&#8203;5520])
-   sync: add `RwLockWriteGuard::{downgrade_map, try_downgrade_map}` ([#&#8203;5527])
-   task: add `JoinHandle::abort_handle` ([#&#8203;5543])

##### Changed

-   io: use `memchr` from `libc` ([#&#8203;5558])
-   macros: accept path as crate rename in `#[tokio::main]` ([#&#8203;5557])
-   macros: update to syn 2.0.0 ([#&#8203;5572])
-   time: don't register for a wakeup when `Interval` returns `Ready` ([#&#8203;5553])

##### Fixed

-   fs: fuse std iterator in `ReadDir` ([#&#8203;5555])
-   tracing: fix `spawn_blocking` location fields ([#&#8203;5573])
-   time: clean up redundant check in `Wheel::poll()` ([#&#8203;5574])

##### Documented

-   macros: define cancellation safety ([#&#8203;5525])
-   io: add details to docs of `tokio::io::copy[_buf]` ([#&#8203;5575])
-   io: refer to `ReaderStream` and `StreamReader` in module docs ([#&#8203;5576])

[#&#8203;5512]: tokio-rs/tokio#5512

[#&#8203;5514]: tokio-rs/tokio#5514

[#&#8203;5520]: tokio-rs/tokio#5520

[#&#8203;5525]: tokio-rs/tokio#5525

[#&#8203;5527]: tokio-rs/tokio#5527

[#&#8203;5540]: tokio-rs/tokio#5540

[#&#8203;5543]: tokio-rs/tokio#5543

[#&#8203;5553]: tokio-rs/tokio#5553

[#&#8203;5555]: tokio-rs/tokio#5555

[#&#8203;5557]: tokio-rs/tokio#5557

[#&#8203;5558]: tokio-rs/tokio#5558

[#&#8203;5559]: tokio-rs/tokio#5559

[#&#8203;5572]: tokio-rs/tokio#5572

[#&#8203;5573]: tokio-rs/tokio#5573

[#&#8203;5574]: tokio-rs/tokio#5574

[#&#8203;5575]: tokio-rs/tokio#5575

[#&#8203;5576]: tokio-rs/tokio#5576

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNS4yNC41IiwidXBkYXRlZEluVmVyIjoiMzUuMjQuNSJ9-->

Co-authored-by: cabr2-bot <cabr2.help@gmail.com>
Reviewed-on: https://codeberg.org/Calciumdibromid/CaBr2/pulls/1838
Reviewed-by: crapStone <crapstone@noreply.codeberg.org>
Co-authored-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
Co-committed-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
d-e-s-o added a commit to d-e-s-o/websocket-util that referenced this pull request Jun 23, 2023
With tokio 1.27.0, the Interval type changed in backwards incompatible
ways, now necessitating a call to Interval::reset to schedule another
wake up [0].
This change makes sure that we follow the new procedure.

[0]: tokio-rs/tokio#5553
d-e-s-o added a commit to d-e-s-o/websocket-util that referenced this pull request Jun 23, 2023
With tokio 1.27.0, the Interval type changed in backwards incompatible
ways, now necessitating a call to Interval::reset to schedule another
wake up [0].
This change makes sure that we follow the new procedure.

[0]: tokio-rs/tokio#5553
@d-e-s-o
Copy link
Contributor

d-e-s-o commented Jun 23, 2023

For what it's worth, I would say that this change is most certainly backwards compatibility breaking, as now an additional call is necessary to get the previous behavior when using the Interval API (behavior which had been there since tokio 1.0, I suppose).
I can see why it's been done, but such a change in behavior can cause tricky production issues. I am not sure how to reconcile this with any semantic versioning guarantees that I understand tokio provides, to be honest :-(

@d-e-s-o
Copy link
Contributor

d-e-s-o commented Jun 23, 2023

The more appropriate course of action, from my perspective, would have been to introduce a new method with the new behavior, deprecate the existing poll_tick, and then after a couple of minor releases or so, replace the old poll_tick with the new one (or remove the old one), for example (I would guess you have a policy in place for deprecations). I understand that this is overhead, but that's the price to pay with 1.0 crates, in my opinion (not that it's specific to them, just that the barrier for 2.0 is probably higher than for incompatible changes before 1.0).

@Darksonn
Copy link
Contributor

Darksonn commented Jun 26, 2023

@d-e-s-o As far as I understand, your code was already broken. The interval would not update the waker when it returns Poll::Ready, so if you change which waker the advance method uses, then it would wake the wrong waker. Similarly, if the next tick is immediately ready, then I don't think you would ever get a wakeup, even if its the same waker (whether that can happen depends on which missed tick behavior you're using).

To add to that, your fix is incorrect. The reset method is not supposed to schedule for wakeup. The correct fix is to call poll_tick again, because poll methods generally only guarantee that you will get a wakeup when they return Poll::Pending. The easiest way to do that is probably to call ctx.waker().wake_by_ref().

To be clear, this is how all poll methods work. It's not something special to Interval. Also, you don't need to bump the Tokio dependency if you use it correctly — the old implementation will still work if you call poll_tick again.

I am not sure how to reconcile this with any semantic versioning guarantees that I understand tokio provides, to be honest :-(

This change is a bug fix. Bug fixes are always breaking changes.

Of course, there is always a trade-off between fixing bugs and keeping broken behavior.

@d-e-s-o
Copy link
Contributor

d-e-s-o commented Jun 26, 2023

Ah okay. Thanks for the explanation!

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 M-time Module: tokio/time R-loom Run loom tests on this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

tokio::time::Interval::poll_tick(...) scheduling a wake-up even when returning Poll::Ready(...)?
4 participants