Search code examples
rustasync-awaitrust-tokio

How to correctly poll after WouldBlock?


I am developing Rust Tokio library for ISO-TP. CAN protocol, which lets you send larger messages. The program is aimed towards linux only.

For this, I am using Tokio structure AsyncFd. When the write is called, I create the Future and then poll it. The problem is when I do two consecutive writes, one after the other.

socket_tx1.write_packet(packet.clone())?.await?;
socket_tx1.write_packet(packet.clone())?.await?;

The first write will end successfully, however second will end with

std::io::ErrorKind::WouldBlock

Which is OK and expected. The buffer is full and we should wait until it's clear and ready for the next write. The poll does not guarantee, that if it returns OK, the following write will be successful.

The problem is that I don't know how to handle this behavior correctly.

I tried the following implementations:

impl Future for IsoTpWriteFuture {
    type Output = io::Result<()>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        loop {
            let guard = ready!(self.socket.0.poll_write_ready(cx))?;
            match self.socket.0.get_ref().0.write(&self.packet) {
                Err(err) if err.kind() == io::ErrorKind::WouldBlock => continue,
                Ok(_) => return Poll::Ready(Ok(())),
                Err(err) => return Poll::Ready(Err(err))
            }
        }
    }
}

This one works, but after I get WouldBlock, this loop results in busy waiting, which I would like to avoid. Since Socket is ready from poll perspective, write is immediately called, Wouldblock is again returned, and routine spins sometime before resolving the write.

The second implementation is more correct, from my perspective, but it doesn't work right now, and I am not sure how to make it work.

impl Future for IsoTpWriteFuture {
    type Output = io::Result<()>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        loop {
            let guard = ready!(self.socket.0.poll_write_ready(cx))?;
            match guard.try_io(|inner|inner.get_ref().0.write(&self.packet)) {
                Err(err) => continue,
                Ok(_) => return Poll::Ready(Ok(())),
            }
        }
    }
}

This doesn't work since once try_io() encounters WouldBlock, it will clear readiness from guard. And since poll is edge triggered, this will hang at poll_write_ready and won't make progress.

Is it possible to poll for change after the write returns WouldBlock? Or is the bussy waiting approach unavoidable?


Solution

  • It was not working due to epoll bug in Linux kernel. I tested it on Kernel 6.6.5 and it was working as expected.