Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

non-blocking stream & io::Write::write_all method are not compatible and result in unexpected user behavior #115483

Closed
softstream-link opened this issue Sep 2, 2023 · 6 comments
Labels
A-docs Area: Documentation for any part of the project, including the compiler, standard library, and tools A-io Area: `std::io`, `std::fs`, `std::net` and `std::path` T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@softstream-link
Copy link

softstream-link commented Sep 2, 2023

Location

This is a continuation of #115451

Summary

The following block of code has unexpected behavior as it will likely not write_all bytes to the stream but there is no way for user to know who many if any where written.

Perhaps the trait should take a mut buf: &[u8] so that the user can check? In either case it is a very hard issue to troubleshot as it fails only on some platforms and under certain conditions yet there is no mention anywhere that nonblocking sockets and io::Write ::write_all method are not compatible.

let mut stream = TcpStream::connect(add).unwrap();
stream.nonblocking(true).unwrap()
let buf = [1,2,3,4,5];
// THIS LINE WILL sometime propagate a WouldBlock error kind however it would have written X number of bytes to the steam with out a way too tell if X is =0 or X<buf.len()
stream.write_all(&buf[..]);  
@softstream-link softstream-link added the A-docs Area: Documentation for any part of the project, including the compiler, standard library, and tools label Sep 2, 2023
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Sep 2, 2023
@the8472
Copy link
Member

the8472 commented Sep 2, 2023

The documentation for write_all says

This method will continuously call write until there is no more data to be written or an error of non-ErrorKind::Interrupted kind is returned.

And the documentation for set_nonblocking says:

If the IO operation could not be completed and needs to be retried, an error with kind io::ErrorKind::WouldBlock is returned.

Combining those two facts means that write_all will abort due to an error if the socket would have to block.
If you want to use non-blocking IO you'll have to use write instead of write_all.

The correct pattern is:

  1. call write until WouldBlock or buffer is empty
  2. wait for readiness via poll/epoll/kqueue and similar (not provided by std)
  3. goto 1

@the8472 the8472 added T-libs Relevant to the library team, which will review and decide on the PR/issue. A-io Area: `std::io`, `std::fs`, `std::net` and `std::path` and removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels Sep 2, 2023
@est31
Copy link
Member

est31 commented Sep 3, 2023

If you want to do non-blocking io, there is great crates in the ecosystem that offer it really well, e.g. tokio.

@softstream-link
Copy link
Author

The documentation for write_all says

This method will continuously call write until there is no more data to be written or an error of non-ErrorKind::Interrupted kind is returned.

And the documentation for set_nonblocking says:

If the IO operation could not be completed and needs to be retried, an error with kind io::ErrorKind::WouldBlock is returned.

Combining those two facts means that write_all will abort due to an error if the socket would have to block. If you want to use non-blocking IO you'll have to use write instead of write_all.

The correct pattern is:

  1. call write until WouldBlock or buffer is empty
  2. wait for readiness via poll/epoll/kqueue and similar (not provided by std)
  3. goto 1

if you read the documentation verbatim I suggests that a retry of write_all is possible after receiving a WouldBlock error. However, it is not possible to recover from this error by trying again given that the session has been corrupted and with a half completed write so trying to write_all again will just continue to corrupt an already corrupted sequence of bytes the receiver expects.

Either method shall be documented to say it is not possible to use write_all with non-blocking to transfer a sequence of bytes over a network without a possibility of corrupting data being sent.

@the8472
Copy link
Member

the8472 commented Sep 5, 2023

However, it is not possible to recover from this error by trying again

Certainly. But that means this combination of methods isn't usable. The docs don't have to spell out every possible combination of uses on all methods. If one API demands something of you and that can't be fulfilled by using A then perhaps you have to use B instead. That is the kind of reasoning that a human programmer has to do all the time or that more complex example programs / tutorials are useful for.

We can document this anyway. But as general expectation you shouldn't assume that every method documents its interaction with every other method.

@quixoticaxis
Copy link

Certainly. But that means this combination of methods isn't usable. The docs don't have to spell out every possible combination of uses on all methods. If one API demands something of you and that can't be fulfilled by using A then perhaps you have to use B instead. That is the kind of reasoning that a human programmer has to do all the time or that more complex example programs / tutorials are useful for.

Non-blocking stream being another type would really help in lifting the burden, IMHO. This ship has sailed, though. TcpStream is considered fully stable and not open to changes, right?

@the8472
Copy link
Member

the8472 commented Sep 5, 2023

Yes. But this isn't specific to non-blocking IO anyway. Any time you need to recover from partial writes or need finer control over the block sizes you'll want to use write instead of write_all anyway. non-blocking IO is merely the most common instance of that.
Sometimes you'll also have to use some sendmsg implementation instead of write. This the consequence of those types being rather thin abstractions over their underlying file descriptors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-docs Area: Documentation for any part of the project, including the compiler, standard library, and tools A-io Area: `std::io`, `std::fs`, `std::net` and `std::path` T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

5 participants