Heap-Hop edited issue #9667:
Returning
0
forstd::io::ErrorKind::WouldBlock
causes downstream to interpret it as a closed stream, see:
https://github.com/yoshuawuyts/wstd/issues/25#issuecomment-2493294087.
https://github.com/yoshuawuyts/wstd/blob/5ce367add5e7bcb569b6487453cb9ba94468dc99/src/io/copy.rs#L12This is also found in:
https://github.com/bytecodealliance/wasmtime/blob/67674881db5fbdbba8594feb52655aaa351a5f77/crates/test-programs/src/bin/preview2_tcp_streams.rs#L17-L22
Given that thewit
files already include manywould-block
errors, would it make sense to extendstream-error
to include awould-block
?
Heap-Hop commented on issue #9667:
pchickey commented on issue #9667:
Thanks! Its a holiday weekend here so I’ll get to this next week.
alexcrichton commented on issue #9667:
I apologize it's taken me quite some time to take a look at this. Now that I've read over this and https://github.com/bytecodealliance/wasmtime/pull/9691 I personally don't think there's a bug in the wasmtime-wasi implementation here. I don't think WASI can reasonably provide super strict guarantees about exactly when things become ready and exactly the states before/after reads. Attempting to do this might not even be feasible in the face of an implementation across multiple platforms.
For example the test in https://github.com/bytecodealliance/wasmtime/pull/9691 looks like this:
use test_programs::wasi::clocks::monotonic_clock::subscribe_duration; let timeout_100ms = 100_000_000; // Send some data to the server client.output.blocking_write_and_flush(b"Hi!").unwrap(); server.input.subscribe().block(); let res = server.input.read(512).unwrap(); assert_eq!(res, b"Hi!", "Expected to receive data"); // Don't send any data let res = server .input .subscribe() .block_until(&subscribe_duration(timeout_100ms)); assert!(res.is_err(), "Expected to time out cause no data was sent");
I don't believe that this can be made to work reliably. Here after the initial "Hi!" message is ready it's not actually known whether the stream is readable or not. Determining this would involve actually consulting the kernel with some sort of syscall or something like that. It's a pretty important perf optimization that in async servers you don't do anything with the kernel until absolutely necessary.
For example here the kernel was consulted during the first
server.input.subscribe().block()
. At this point the stream is considered readable. The issuedread
then again consults the kernel and reads out"Hi!"
. At this point though the stream is notably considered still readable by Tokio and Wasmtime. It's not known whether this was a "short read" or whether more data is coming soon. This is why the nextsubscribe()
immediately says "yes this is readable". Doing otherwise would require unnecessarily consulting the kernel.Working robustly with async is certainly an art but personally I don't think that there's a bug here that needs fixing. I would personally think we can close https://github.com/bytecodealliance/wasmtime/pull/9691 and this issue as "working as intended".
pchickey commented on issue #9667:
I apologize I never got back to this after saying I would. I started a new job and have a whole host of new problems to work on, so I dropped some of the old ones.
Thanks Alex for digging into this and helping me understand why the existing behavior is there. My conclusion is that my specification of "a ready pollable implies input-stream.read will succeed" is too strong of a guarantee to implement efficiently on top of POSIX. In discussion with Alex, he suggested that only the behavior of read shows whether input was ready or not, and the pollable's readiness is a best-effort indicator that provides an optimization over busy-looping on read. I expect that we can easily accommodate the weakening of that guarantee in existing code (at least in wstd), and any existing binaries can't possibly be relying on that guarantee since its never been upheld by implementations. We will have to take that discussion out to the spec repo in order to nail it all down.
badeend commented on issue #9667:
I would personally think we can close (...) this issue as "working as intended".
For non-blocking APIs: agree. There's not much wasmtime can do (efficiently).
Maybe this could also justify the removal of starting an arbitrary-sized read in the FileInputStream implementation that was put in place to fulfill this guarantee?
For blocking APIs: there a few places where wasmtime blindly trusts that
ready().await
actually implies readiness. For example:In these cases wasmtime _is_ in a position to "fix" spurious wakeups by wrapping it in a loop.
alexcrichton commented on issue #9667:
Yeah I would agree that
blocking_read
should have a loop to handle situations like this
Heap-Hop commented on issue #9667:
This is why the next subscribe() immediately says "yes this is readable". Doing otherwise would require unnecessarily consulting the kernel.
@alexcrichton Thank you so much for the clear explanation, it resolved my confusion about why
readable()
in Tokio is not explicit.It would be great if the
blocking_
functions returned the expected results.I would be happy to submit a PR to modify the
blocking_
functions if no one else is currently working on it.
alexcrichton commented on issue #9667:
That'd be most welcome, thank you!
Last updated: Jan 24 2025 at 00:11 UTC