Skip to content

write_all doesn't guarantee data to be written after returning Poll::Ready #5531

@mineichen

Description

@mineichen

Version
Affects all tokio-versions since 1.0

Platform
Veryfied on

  • 5.15.81-1-MANJARO x86_64 GNU/Linux (1.65)
  • 64-bit Windows 11 (RUST 1.67.1)

Description
tokio::io::AsyncWriteExt::write_all doesn't guarantee to write all data on it's completion. The following sometimes fails in the first cycle, sometimes after several hundred iterations:

[dependencies]
futures = "0.3.25"
tokio = {version = "1.26.0", features = ["fs", "io-util", "macros", "rt", "time"]}
tokio-util = {version = "0.7.7", features = ["compat"]}

#[cfg(test)]
mod tests {

    #[tokio::test]
    async fn tokio_read_write() -> Result<(), Box<dyn std::error::Error>> {
        for i in 0..10_000 {
            let filepath = "settings.json";
            tokio::fs::File::create(filepath).await?;
            {
                // By using this instead of tokio::write_all, the test alwas exits successfully 
                // std::io::Write::write_all(&mut std::fs::File::create(filepath)?, b"test")?;
                
                tokio::io::AsyncWriteExt::write_all(
                    &mut tokio::fs::File::create(&filepath).await?,
                    b"test",
                )
                .await?; 

                // Is broken too:
                /*
                futures::AsyncWriteExt::write_all(
                    &mut tokio_util::compat::TokioAsyncWriteCompatExt::compat_write(
                        tokio::fs::File::create(&filepath).await?,
                    ),
                    b"test",
                )
                .await?; */
            }
            let mut data = Vec::new();
            // Both commands fail with tokio::write_all
            // let count = std::fs::File::open(filepath).and_then(|mut f| f.read_to_end(&mut data));
            let count = match tokio::fs::File::open(filepath).await {
                Ok(mut f) => tokio::io::AsyncReadExt::read_to_end(&mut f, &mut data).await,
                Err(e) => Err(e),
            };

            if let Ok(x) = count {
                if x == 0 {
                    let now_it_might_work = match tokio::fs::File::open(filepath).await {
                        Ok(mut f) => tokio::io::AsyncReadExt::read_to_end(&mut f, &mut data).await,
                        Err(e) => Err(e),
                    };
                    panic!("READ bytes run {i}: {:?}", now_it_might_work);
                }
            }
        }
        Ok(())
    }
}

Ideas
The implementation WriteAll seems suspicious to me:

impl<W> Future for WriteAll<'_, W>
where
    W: AsyncWrite + Unpin + ?Sized,
{
    type Output = io::Result<()>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        let me = self.project();
        while !me.buf.is_empty() {
            let n = ready!(Pin::new(&mut *me.writer).poll_write(cx, me.buf))?;
            {
                let (_, rest) = mem::take(&mut *me.buf).split_at(n);
                *me.buf = rest;
            }
            if n == 0 {
                return Poll::Ready(Err(io::ErrorKind::WriteZero.into()));
            }
        }

        Poll::Ready(Ok(()))
    }
}

Shouldn't the last line be replaced with the following?

Pin::new(&mut *me.writer).poll_flush(cx)

If so, futures::AsyncWriteExt::write_all has to be patched as well.

Severeness
I think that this behavior is very unexpected and thus dangerous. If it cannot be fixed, it should at least be documented in the docs.

EDIT 30min later:
A call to tokio::io::AsyncWriteExt::flush(&mut file).await?; after the write_all solves the issue. I see why the flush doesn't happen automatically. This is redundant work on consecutive calls to write_all. I would however not expect a helper-function to require me to call flush afterwards. Imho there should be a write_all_unflashed with the current implementation and the write_all should contain the flush. Code could still be optimized without users running into this trap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-tokioArea: The main tokio crateC-bugCategory: This is a bug.M-fsModule: tokio/fs

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions