-
Notifications
You must be signed in to change notification settings - Fork 653
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
Ensuring NIOAsyncChannel flushes all writes before closing #3049
base: main
Are you sure you want to change the base?
Conversation
…e flushed pipeline already.
// This is always called from an async context, so we must loop-hop. | ||
// Because we always loop-hop, we're always at the top of a stack frame. As this | ||
// is the only source of writes for us, and as this channel handler doesn't implement | ||
// func write(), we cannot possibly re-entrantly write. That means we can skip many of the | ||
// awkward re-entrancy protections NIO usually requires, and can safely just do an iterative | ||
// write. | ||
self.eventLoop.preconditionInEventLoop() | ||
guard let context = self.context else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test has been moved into _doOutboundWrites as we need to complete promises even if the channel handler is no longer there.
// This is always called from an async context, so we must loop-hop. | ||
// Because we always loop-hop, we're always at the top of a stack frame. As this | ||
// is the only source of writes for us, and as this channel handler doesn't implement | ||
// func write(), we cannot possibly re-entrantly write. That means we can skip many of the | ||
// awkward re-entrancy protections NIO usually requires, and can safely just do an iterative | ||
// write. | ||
self.eventLoop.preconditionInEventLoop() | ||
guard let context = self.context else { | ||
// Already removed from the channel by now, we can stop. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test has been moved into _doOutboundWrites as we need to complete promises even if the channel handler is no longer there.
Thanks for tackling this. I and the rest of the team agree that this is a problem but we are not yet sure how we want to fix it. There are three closely related problems that have to be tackled together to from a good overall async interop story.
We want to tackle all of them together to provide an API that achieves:
In this PR you are tackling the problem of the writes which I understand you are hitting in Hummingbird right now; however, the other open problems are unhandled. Just to be clear I don't expect you to tackle those. For the writes we have discussed four different approaches in the team so far:
|
The problem with not extending the public PR is you are trying to match a single The channel pipeline write APIs provide all this functional flexibility. Is there a reason NIOAsyncChannel is wanting to hide it all?
Please do not do this. The performance implications are dreadful. I did think about doing something a little more internal where any write that could be considered a final write would have a promise created for it internally, but the write function wouldn't wait on this promise completing but it would be stored "somewhere" and when we get to the executeAndClose {} closing phase it would wait on the last promise allocated for a write. As I understand it the only way to verify a write has made it all the way through the pipeline is to create a promise for it. I think you'll need a higher level API to indicate the possibility of a write being the final write to avoid creating promises for ever write. |
The main reason is that one of the most common NIO bugs in real code is forgetting to flush. |
Currently NIOAsyncChannel can close the channel without having flushed all the writes it has made.
Motivation:
I believe there are two situations where writes are not getting written. At the point the NIOAsyncChannel starts the channel close procedure it calls
finish
on the writer which will resume all the pending continuations but then closes the channel immediately so those writes are never written. The second situation is related to the fact that there is no way to wait on a write actually making it all the way through the channel pipeline.These proposed changes are more to start a discussion and this PR is still very much a draft.
Modifications:
Instead of passing Values through the NIOAsyncWriter we pass actions. There are currently three actions
write
: write value to channelwriteAndFlush
: write value to channel and flush the pipeline (returning when write promise either fails or succeeds).flush
: Ensure all previous writes have been written.Result:
As long as the last write a user makes is with
writeAndFlush
everything will get written out before the channel is closed.