Serialize socket writes + consider client pending completions when sh… #458
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
…utting down
Previously, we could have multiple in-flight messages from the server to a single client. This isn't safe and can lead to message interleaving. While write / send are atomic, they are only atomic for the N bytes which they write, which may not be the entire buffer. Consider this writeAll function:
If we're trying to send "abc123", this could take anywhere from 1 to 6 calls to posix.write (it would take 6 calls, for example, if every call to posix.write only wrote a single byte). Now if you're trying to write other data to this same socket at the same time, messages will get interleaved.
In order for this to work, the client now has a send_queue (doubly linked list). When one message is sent, it sends the next.
In addition to the above change, the Client is now self-contained with respect to its lifetime. This is necessary so that completions which come in AFTER our concept of its lifetime ends, can still be processed. I think all types that receive completions need to follow this model. This relies on the fact that kqueue (which I know for a fact) and io_uring (which people seem to imply) handle socket shutdown properly. It's still a bit messy because of timeout and not wanting to wait until timeout to accept new connections, but needing to wait until timeout to cleanup the client.
The self-contained nature of Client makes it difficult to test as a generic. I removed Client(T). Tests now use real sockets. Some tests had to be removed because they're too difficult to test over a real connection :(