Repository: tokio-rs/tokio (Rust)

Issue:
The `tokio::io::empty` combinator does not participate in coop
**Version**
List the versions of all `tokio` crates you are using. The easiest way to get
this information is using `cargo tree` subcommand:

`cargo tree | grep tokio`
```
└── tokio v1.14.0
    └── tokio-macros v1.6.0 (proc-macro)

```

**Platform**
The output of `uname -a` (UNIX), or version and 32 or 64-bit (Windows)
`Linux francis-debian 5.10.0-9-amd64 #1 SMP Debian 5.10.70-1 (2021-09-30) x86_64 GNU/Linux`

**Description**

I tried this code:

```rust
use std::time::Duration;
use tokio::io::AsyncReadExt;
use tokio::time::timeout;

#[tokio::main]
async fn main() {
    if timeout(Duration::from_millis(1000), async {
        loop {
            let mut buf = [0u8; 4096];
            let _ = tokio::io::empty().read(&mut buf).await;
        }
    }).await.is_err() {
        println!("timedout");
    };
}
```

I expected to see this happen:

I expected a timeout to occur after 1 second and for the process to exit. Instead it hangs forever. 



Discussion:
Reading from `io::empty` always returns immediately, so the loop will never yield. The timeout cannot fire unless the main task yields, because it's blocking the thread.

If you spawn the `async` block inside the timeout as a separate task, you should see it time out as expected: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8c1de2e3ce490e8d57ff848f32ae95db
It occurs to me that `io::empty` should maybe participate in [cooperative yielding](https://tokio.rs/blog/2020-04-preemption) to prevent busy-loops like this from hanging the entire runtime...
Thanks for reporting this. We should make sure that `io::empty` and friends participate in coop.
Possibly related/same thing:

```rust
use std::process::Stdio;
use std::time::Duration;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
use tokio::time::timeout;

#[tokio::main]
async fn main() {
    let mut command = Command::new("yes");
     command.stdout(Stdio::piped());
    let mut spawned = command.spawn().expect("error spawning");
    let mut stdout = spawned.stdout.take().expect("no stdout?");
    if timeout(Duration::from_millis(1000), async {
        loop {
            let mut buf = [0u8; 1];
            let _ = stdout.read(&mut buf).await;
        }
    }).await.is_err() {
        println!("timedout");
    };
}

```


Assuming you have a fast enough yes on your system, then the above also doesn't timeout when it should.
Actually the original issue isn't fixed yet because of https://github.com/tokio-rs/tokio/pull/4300#issuecomment-986769404 . I should have updated the PR body. Anyway, I'm working on a PR that finally fixes this problem.

`tokio::io::empty` does participate in coop now, which is what the currrent title of the issue refers to, so I guess it could make sense to keep it closed.



Patch:
diff --git a/tokio/src/io/util/empty.rs b/tokio/src/io/util/empty.rs
index f964d18e6ef..77db60e40b4 100644
--- a/tokio/src/io/util/empty.rs
+++ b/tokio/src/io/util/empty.rs
@@ -50,16 +50,18 @@ impl AsyncRead for Empty {
     #[inline]
     fn poll_read(
         self: Pin<&mut Self>,
-        _: &mut Context<'_>,
+        cx: &mut Context<'_>,
         _: &mut ReadBuf<'_>,
     ) -> Poll<io::Result<()>> {
+        ready!(poll_proceed_and_make_progress(cx));
         Poll::Ready(Ok(()))
     }
 }
 
 impl AsyncBufRead for Empty {
     #[inline]
-    fn poll_fill_buf(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
+    fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> {
+        ready!(poll_proceed_and_make_progress(cx));
         Poll::Ready(Ok(&[]))
     }
 
@@ -73,6 +75,20 @@ impl fmt::Debug for Empty {
     }
 }
 
+cfg_coop! {
+    fn poll_proceed_and_make_progress(cx: &mut Context<'_>) -> Poll<()> {
+        let coop = ready!(crate::coop::poll_proceed(cx));
+        coop.made_progress();
+        Poll::Ready(())
+    }
+}
+
+cfg_not_coop! {
+    fn poll_proceed_and_make_progress(_: &mut Context<'_>) -> Poll<()> {
+        Poll::Ready(())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;


Test patch:
diff --git a/tokio/tests/io_util_empty.rs b/tokio/tests/io_util_empty.rs
new file mode 100644
index 00000000000..e49cd17fcd5
--- /dev/null
+++ b/tokio/tests/io_util_empty.rs
@@ -0,0 +1,32 @@
+#![cfg(feature = "full")]
+use tokio::io::{AsyncBufReadExt, AsyncReadExt};
+
+#[tokio::test]
+async fn empty_read_is_cooperative() {
+    tokio::select! {
+        biased;
+
+        _ = async {
+            loop {
+                let mut buf = [0u8; 4096];
+                let _ = tokio::io::empty().read(&mut buf).await;
+            }
+        } => {},
+        _ = tokio::task::yield_now() => {}
+    }
+}
+
+#[tokio::test]
+async fn empty_buf_reads_are_cooperative() {
+    tokio::select! {
+        biased;
+
+        _ = async {
+            loop {
+                let mut buf = String::new();
+                let _ = tokio::io::empty().read_line(&mut buf).await;
+            }
+        } => {},
+        _ = tokio::task::yield_now() => {}
+    }
+}