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

watch::Receiver::changed does use cooperative scheduling #6839

Closed
vthib opened this issue Sep 12, 2024 · 1 comment · Fixed by #6846
Closed

watch::Receiver::changed does use cooperative scheduling #6839

vthib opened this issue Sep 12, 2024 · 1 comment · Fixed by #6846
Labels
A-tokio Area: The main tokio crate C-bug Category: This is a bug. M-coop Module: tokio/coop M-sync Module: tokio/sync

Comments

@vthib
Copy link

vthib commented Sep 12, 2024

Version

tokio 1.40.0

Platform

Linux hostname 6.10.8-arch1-1 #1 SMP PREEMPT_DYNAMIC Wed, 04 Sep 2024 15:16:37 +0000 x86_64 GNU/Linux

Description

I stumbled upon a surprising behavior that caused a program to never end, because dropping
the runtime never ended.

The behavior can be better explained with this reproducer:

fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();

    // With an mpsc, there is no blocking on runtime drop
    // With a watch, the runtime drop can block infinitely.

    // let (sender, mut receiver) = tokio::sync::mpsc::channel::<()>(1);
    let (sender, mut receiver) = tokio::sync::watch::channel(false);

    // This task will block the runtime during drop
    rt.spawn(async move {
        loop {
            // let _r = receiver.recv().await;
            let _r = receiver.changed().await;
        }
    });
    drop(sender);

    println!("drop runtime");
    drop(rt);
    println!("runtime has been dropped");
}

Running this, "drop runtime" is printed but "runtime has been dropped" is never printed and the program never ends. This is because the task spin loops on the call to receiver.changed().await which returns an error.

It would be expected that this .await would yield and thus allow the runtime to drop the task, but
that does not appear to be the case. This is afaict surprising behavior from my understanding on how shutting down the runtime is documented, especially given that for other types of queues (for example mpsc), the recv() call does yield even if the sender has been dropped for example.

Of course the code isn't ideal here and the bug can clearly be avoided, but I thought it would be interesting to report. Either this is a bug and the changed().await should yield, or maybe the documentation on this function should make it clear that in the case where the sender has been dropped, this call does not yield?

@vthib vthib added A-tokio Area: The main tokio crate C-bug Category: This is a bug. labels Sep 12, 2024
@Darksonn Darksonn added M-sync Module: tokio/sync M-coop Module: tokio/coop labels Sep 12, 2024
@Darksonn
Copy link
Contributor

It's entirely normal that async operations don't yield if they can complete immediately. However, we do have a mechanism to avoid this kind of bug called cooperative scheduling, which we should apply to the watch channel.

@Darksonn Darksonn changed the title watch::Receiver::changed does not yield when sender has been dropped watch::Receiver::changed does use cooperative scheduling Sep 12, 2024
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. M-coop Module: tokio/coop M-sync Module: tokio/sync
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants