-
Notifications
You must be signed in to change notification settings - Fork 654
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
Open
adam-fowler
wants to merge
9
commits into
apple:main
Choose a base branch
from
adam-fowler:async-channel-flush-writes
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
3bb0761
Add flush and writeAndFlush to NIOAsyncChannelOutboundWriter
adam-fowler f59a045
Still parse flush outboundActions when channel is closed
adam-fowler f695456
Add withPromise
adam-fowler 53575b5
Add testAllWritesAreWritten
adam-fowler 8604b9e
Update comments
adam-fowler d35055f
Remove `flush` from `AsyncChannel.executeAndClose` as user should hav…
adam-fowler ce2b566
Add writeAndFlush(contentsOf:)
adam-fowler 294098d
Remove flush from executeThenClose error handling
adam-fowler 9afbf77
Move eventLoop as associated value of writer case
adam-fowler File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,16 @@ | |
|
||
import DequeModule | ||
|
||
@usableFromInline | ||
enum OutboundAction<OutboundOut>: Sendable where OutboundOut: Sendable { | ||
/// Write value | ||
case write(OutboundOut) | ||
/// Write value and flush pipeline | ||
case writeAndFlush(OutboundOut, EventLoopPromise<Void>) | ||
/// flush writes to writer | ||
case flush(EventLoopPromise<Void>) | ||
} | ||
|
||
/// A ``ChannelHandler`` that is used to transform the inbound portion of a NIO | ||
/// ``Channel`` into an asynchronous sequence that supports back-pressure. It's also used | ||
/// to write the outbound portion of a NIO ``Channel`` from Swift Concurrency with back-pressure | ||
|
@@ -77,7 +87,7 @@ internal final class NIOAsyncChannelHandler<InboundIn: Sendable, ProducerElement | |
|
||
@usableFromInline | ||
typealias Writer = NIOAsyncWriter< | ||
OutboundOut, | ||
OutboundAction<OutboundOut>, | ||
NIOAsyncChannelHandlerWriterDelegate<OutboundOut> | ||
> | ||
|
||
|
@@ -372,7 +382,10 @@ struct NIOAsyncChannelHandlerProducerDelegate: @unchecked Sendable, NIOAsyncSequ | |
|
||
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) | ||
@usableFromInline | ||
struct NIOAsyncChannelHandlerWriterDelegate<Element: Sendable>: NIOAsyncWriterSinkDelegate, @unchecked Sendable { | ||
struct NIOAsyncChannelHandlerWriterDelegate<OutboundOut: Sendable>: NIOAsyncWriterSinkDelegate, @unchecked Sendable { | ||
@usableFromInline | ||
typealias Element = OutboundAction<OutboundOut> | ||
|
||
@usableFromInline | ||
let eventLoop: EventLoop | ||
|
||
|
@@ -386,7 +399,7 @@ struct NIOAsyncChannelHandlerWriterDelegate<Element: Sendable>: NIOAsyncWriterSi | |
let _didTerminate: ((any Error)?) -> Void | ||
|
||
@inlinable | ||
init<InboundIn, ProducerElement>(handler: NIOAsyncChannelHandler<InboundIn, ProducerElement, Element>) { | ||
init<InboundIn, ProducerElement>(handler: NIOAsyncChannelHandler<InboundIn, ProducerElement, OutboundOut>) { | ||
self.eventLoop = handler.eventLoop | ||
self._didYieldContentsOf = handler._didYield(sequence:) | ||
self._didYield = handler._didYield(element:) | ||
|
@@ -430,35 +443,27 @@ struct NIOAsyncChannelHandlerWriterDelegate<Element: Sendable>: NIOAsyncWriterSi | |
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) | ||
extension NIOAsyncChannelHandler { | ||
@inlinable | ||
func _didYield(sequence: Deque<OutboundOut>) { | ||
func _didYield(sequence: Deque<OutboundAction<OutboundOut>>) { | ||
// 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. | ||
return | ||
} | ||
|
||
self._doOutboundWrites(context: context, writes: sequence) | ||
} | ||
|
||
@inlinable | ||
func _didYield(element: OutboundOut) { | ||
func _didYield(element: OutboundAction<OutboundOut>) { | ||
// 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 commentThe 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. |
||
return | ||
} | ||
|
||
self._doOutboundWrite(context: context, write: element) | ||
} | ||
|
@@ -475,18 +480,64 @@ extension NIOAsyncChannelHandler { | |
} | ||
|
||
@inlinable | ||
func _doOutboundWrites(context: ChannelHandlerContext, writes: Deque<OutboundOut>) { | ||
for write in writes { | ||
context.write(Self.wrapOutboundOut(write), promise: nil) | ||
func _doOutboundWrites(context: ChannelHandlerContext?, writes: Deque<OutboundAction<OutboundOut>>) { | ||
// write everything but the last item | ||
for write in writes.dropLast() { | ||
switch write { | ||
case .write(let value), .writeAndFlush(let value, _): | ||
guard let context = self.context else { | ||
// Already removed from the channel by now, we can stop. | ||
return | ||
} | ||
context.write(Self.wrapOutboundOut(value), promise: nil) | ||
context.flush() | ||
case .flush(let promise): | ||
promise.succeed() | ||
} | ||
} | ||
// write last item | ||
switch writes.last { | ||
case .write(let value): | ||
guard let context = self.context else { | ||
// Already removed from the channel by now, we can stop. | ||
return | ||
} | ||
context.write(Self.wrapOutboundOut(value), promise: nil) | ||
context.flush() | ||
case .flush(let promise): | ||
promise.succeed() | ||
case .writeAndFlush(let value, let promise): | ||
guard let context = self.context else { | ||
// Already removed from the channel by now, we can stop. | ||
promise.succeed() | ||
return | ||
} | ||
context.writeAndFlush(Self.wrapOutboundOut(value), promise: promise) | ||
case .none: | ||
break | ||
} | ||
|
||
context.flush() | ||
} | ||
|
||
@inlinable | ||
func _doOutboundWrite(context: ChannelHandlerContext, write: OutboundOut) { | ||
context.write(Self.wrapOutboundOut(write), promise: nil) | ||
context.flush() | ||
func _doOutboundWrite(context: ChannelHandlerContext?, write: OutboundAction<OutboundOut>) { | ||
switch write { | ||
case .write(let value): | ||
guard let context = self.context else { | ||
// Already removed from the channel by now, we can stop. | ||
return | ||
} | ||
context.write(Self.wrapOutboundOut(value), promise: nil) | ||
context.flush() | ||
case .flush(let promise): | ||
promise.succeed() | ||
case .writeAndFlush(let value, let promise): | ||
guard let context = self.context else { | ||
// Already removed from the channel by now, we can stop. | ||
promise.succeed() | ||
return | ||
} | ||
context.writeAndFlush(Self.wrapOutboundOut(value), promise: promise) | ||
} | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
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.