Skip to content

[wasi-sockets] Fresh UDP sockets occasionally return ErrorCode::WouldBlock on first send #12612

@dicej

Description

@dicej

I'm (still) working to add WASIp2 support to mio and have noticed that several of the UDP tests usually pass but sometimes fail. When they fail, it's always due to the host returning error-code::would-block from outgoing-datagram-stream.send (which wasi-libc translates to returning EWOULDBLOCK from send(2)). This can happen even if the socket was just created, has not previously been used to send anything, and for which outgoing-datagram-stream.check-send has returned a non-zero number. It can also happen when poll.poll has just returned a write-ready event for the pollable representing the outgoing-datagram-stream.

I've simplified the test case somewhat here:

// udp.rs
use std::{error::Error, io::ErrorKind, net::UdpSocket};

fn main() -> Result<(), Box<dyn Error>> {
    for _ in 0..50 {
        let socket1 = UdpSocket::bind("127.0.0.1:0")?;
        socket1.set_nonblocking(true)?;
        let socket2 = UdpSocket::bind("127.0.0.1:0")?;
        socket2.set_nonblocking(true)?;

        let address1 = socket1.local_addr()?;
        let address2 = socket2.local_addr()?;

        socket1.connect(address2)?;
        socket2.connect(address1)?;

        let data = b"foobar";
        socket1.send(data)?;

        let mut buffer = [0u8; 20];
        match socket2.recv(&mut buffer) {
            Ok(n) => {
                assert_eq!(data, &buffer[..n]);
            }
            Err(error) => {
                assert!(matches!(error.kind(), ErrorKind::WouldBlock));
            }
        }
    }

    println!("success!");

    Ok(())
}

If you run e.g. rustc --target=wasm32-wasip2 udp.rs && while wasmtime run -Sudp,inherit-network udp.wasm; do :; done, sooner or later you'll see Error: Os { code: 6, kind: WouldBlock, message: "Resource temporarily unavailable" }. However, if you run e.g. rustc udp.rs && while ./udp; do :; done on Linux, it will succeed indefinitely.

When I dug into this, I found that wasmtime-wasi uses tokio::net::UdpSocket::try_send[_to] to send datagrams, which eventually consults this function from here to determine whether to even bother trying to send. Indeed, when I add debug logging to that function and patch Wasmtime to use it, I see that the "not ready" case corresponds to when the host returns would-block to the guest, which indicates that tokio is applying backpressure rather than the OS.

I imagine that in most real-world code, returning spurious would-block errors occasionally wouldn't be a problem; the application can just poll and try again. However, it makes the mio tests flaky, so I'd like to brainstorm solutions here.

One rather heavy-handed approach could be to bypass Tokio for sending and receiving datagrams, using tokio::net::UdpSocket::{into,from}_std to temporarily convert between the Tokio version and the std version. I imagine that could have other implications, though.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIncorrect behavior in the current implementation that needs fixingwasiIssues pertaining to WASI

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions