-
Notifications
You must be signed in to change notification settings - Fork 631
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
Implement a NIOAsyncWriter
#2251
Conversation
3732c95
to
558af8a
Compare
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.
Only just got to the meat of this PR; will pick up the rest of the review later. Leaving my comments so far.
f215798
to
69f14ac
Compare
df36553
to
ac921d4
Compare
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.
Great start. Left some broader questions for now.
var yieldID: UInt64 | ||
/// The yield's produced sequence of elements. | ||
@usableFromInline | ||
var elements: AnySequence<Element> |
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.
Should the AsyncWriter be generic over the Sequence type as well? How often do user's want to send different Sequence types? For the SequenceOfOne/Two case users could always opt for an enum that implements Sequence.
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.
Yeah, I didn't like the point of holding an AnySequence
here. If we do that, then we should get rid of the yielding of a single element.
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.
If we do that, then we should get rid of the yielding of a single element.
I think we must and I consider this a good thing :P
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.
Should the AsyncWriter be generic over the Sequence type as well? How often do user's want to send different Sequence types?
I'm not sure that's a good idea in practice. I think the typical usage pattern would be for a framework to create a writer and pass it to a caller. That would mean the framework would be responsible for choosing what the sequence type is or the caller has to ask the framework for a writer with a given sequence type (which is a pretty awkward API).
For the SequenceOfOne/Two case users could always opt for an enum that implements Sequence.
Not offering a yield-one API and requiring users to implement an enum like this makes for a really user hostile API IMHO.
FWIW I also don't like AnySequence
here but I think we should keep it for the time being (it is an implementation detail and can be changed later).
One alternative would be to always store [Element]
and special case the API to avoid copies when an array is provided and copy into an array when anything else is provided.
Taking that further we could be to pull a 'NIOAny' trick and have an enum of array
, one
, and anySequence
.
That said, I think we should focus on getting the API right first and then compare perf.
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.
I agree with what George said. Making the whole thing generic of the sequence that is yielded severely limits the usage of the type and makes it very awkward to hold. I would stick with AnySequence
for now and as George said we can always make it faster by applying some of our tricks.
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.
As discussed offline, I think we should change this to remove the AnySequence
.
let yieldID = self._yieldIDCounter.loadThenWrappingIncrement(ordering: .relaxed) | ||
|
||
try await withTaskCancellationHandler { | ||
// We are manually locking here to hold the lock across the withCheckedContinuation call | ||
self._lock.lock() | ||
|
||
let action = self._stateMachine.yield(yieldID: yieldID) |
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.
What's the benefit of having the yieldIDCounter
as an atomic, if we acquire a lock afterwards anyway?
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.
We acquire the lock inside the cancellation handler and inside the operation. However, we need to get the ID outside of both of them. We have two options:
- Aquire the lock before once to get an id and then acquire the lock inside again
- Use an atomic for the id
I went with the atomic because that should perform better than the lock.
var yieldID: UInt64 | ||
/// The yield's produced sequence of elements. | ||
@usableFromInline | ||
var elements: AnySequence<Element> |
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.
Should the AsyncWriter be generic over the Sequence type as well? How often do user's want to send different Sequence types?
I'm not sure that's a good idea in practice. I think the typical usage pattern would be for a framework to create a writer and pass it to a caller. That would mean the framework would be responsible for choosing what the sequence type is or the caller has to ask the framework for a writer with a given sequence type (which is a pretty awkward API).
For the SequenceOfOne/Two case users could always opt for an enum that implements Sequence.
Not offering a yield-one API and requiring users to implement an enum like this makes for a really user hostile API IMHO.
FWIW I also don't like AnySequence
here but I think we should keep it for the time being (it is an implementation detail and can be changed later).
One alternative would be to always store [Element]
and special case the API to avoid copies when an array is provided and copy into an array when anything else is provided.
Taking that further we could be to pull a 'NIOAny' trick and have an enum of array
, one
, and anySequence
.
That said, I think we should focus on getting the API right first and then compare perf.
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.
I think we're mishandling the delegate throughout this code.
Because we only call the delegate from outside the lock, we do not guarantee to maintain ordering of yields. We can suspend immediately after we drop the lock, and then have a completely unrelated Task come in and yield results, get an immediate call to didYield
, and then drop into the delegate, beating the first Task.
This didn't matter in the previous delegate, which was only being called with signals that were themselves well-ordered, but it does matter here. We need a fundamentally different behaviour that maintains ordering, probably by way of keeping the lock held across these calls. That will require some thought as to how it interacts with setWritability
, which we need to be careful wtih.
ac921d4
to
a81dbd1
Compare
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.
Looks great! Left a few documentation nits/fixes.
var yieldID: UInt64 | ||
/// The yield's produced sequence of elements. | ||
@usableFromInline | ||
var elements: AnySequence<Element> |
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.
As discussed offline, I think we should change this to remove the AnySequence
.
@Lukasa I changed the API to always yield a |
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) | ||
public struct NIOAsyncWriter< | ||
Element, | ||
Failure, |
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.
Presumably we require that Element
and Failure
are Sendable
, no?
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.
They already are. The delegate is currently restricting the Element
to be Sendable
which propagates to this generic type through the constraints. Furthermore, Failure
is restricted to Error
which inherits from Sendable
.
But I added it here as constraints as well to make it more obvious
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.
Actually taking this back since it produces a warning since it is a duplicate constraint.
# Motivation We previously added the `NIOAsyncProducer` to bridge between the NIO channel pipeline and the asynchronous world. However, we still need something to bridge writes from the asynchronous world back to the NIO channel pipeline. # Modification This PR adds a new `NIOAsyncWriter` type that allows us to asynchronously `yield` elements to it. On the other side, we can register a `NIOAsyncWriterDelegate` which will get informed about any written elements. Furthermore, the synchronous side can toggle the writability of the `AsyncWriter` which allows it to implement flow control. A main goal of this type is to be as performant as possible. To achieve this I did the following things: - Make everything generic and inlinable - Use a class with a lock instead of an actor - Provide methods to yield a sequence of things which allows users to reduce the amount of times the lock gets acquired. # Result We now have the means to bridge writes from the asynchronous world to the synchronous
…nto the state machine
…IOAsyncWriterSinkDelegate
f141fb8
to
7d3e7fa
Compare
@Lukasa @glbrntt @gjcairo I just pushed a new commit that changes a few things after our offline discussion:
|
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 is great @FranzBusch -- I think a couple of docs are out of date and should be fixed in a followup.
Motivation
We previously added the
NIOAsyncSequenceProducer
to bridge between the NIO channel pipeline and the asynchronous world. However, we still need something to bridge writes from the asynchronous world back to the NIO channel pipeline.Modification
This PR adds a new
NIOAsyncWriter
type that allows us to asynchronouslyyield
elements to it. On the other side, we can register aNIOAsyncWriterDelegate
which will get informed about any written elements. Furthermore, the synchronous side can toggle the writability of theAsyncWriter
which allows it to implement flow control.A main goal of this type is to be as performant as possible. To achieve this I did the following things:
Result
We now have the means to bridge writes from the asynchronous world to the synchronous