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

Add AsyncChannel based ServerBootstrap.bind() methods #2403

Merged
merged 11 commits into from
Apr 26, 2023

Conversation

FranzBusch
Copy link
Member

Motivation

In my previous PR, we added a new async bridge from a NIO Channel to Swift Concurrency primitives in the from of the NIOAsyncChannel. This type alone is already helpful in bridging Channels to Concurrency; however, it is hard to use since it requires to wrap the Channel at the right time otherwise we will drop reads. Furthermore, in the case of protocol negotiation this becomes even trickier since we need to wait until it finishes and then wrap the Channel.

Modification

This PR introduces a few things:

  1. New methods on the ServerBootstrap which allow the creation of NIOAsyncChannel based channels. This can be used in all cases where no protocol negotiation is involved.
  2. A new protocol and type called NIOProtocolNegotiationHandler and NIOProtocolNegotiationResult which is used to identify channel handlers that are doing protocol negotiation.
  3. New methods on the ServerBootstrap that are aware of protocol negotiation.

Result

We can now easily and safely create new AsyncChannels from the ServerBootstrap

Sources/NIOPosix/Bootstrap.swift Show resolved Hide resolved
Comment on lines +714 to +709
}.flatMap {
$0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you use eventLoop.flatSubmit you can drop this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where would I use the flatSubmit here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of the submit on L699

Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
Comment on lines +941 to +913
}.flatMap {
$0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here re: flat submit

Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
let switchFuture = self.completionHandler(result, context.channel)
switchFuture
.whenComplete { result in
// We must be in the event loop here to make sure no hops have happened
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an API requirement? Can't we just hop back to the right event-loop?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a tricky one. In theory, yes it is fine to hop here since the user can add the handlers to the pipeline in their closure and then hop to another future.
I am going to remove that preconditions. Users can screw up with weird construction anyhow but hopefully they don't have to wrap into NIOAsyncChannel themselves often and can just use our configureXXX methods.

@FranzBusch FranzBusch marked this pull request as ready for review April 13, 2023 12:52
Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
@FranzBusch FranzBusch requested a review from glbrntt April 14, 2023 09:25
) where InboundIn == ProducerElement {
self.eventLoop = eventLoop
self.closeRatchet = closeRatchet
self.transformation = .sync { $0 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have transformation be an optional instead of having to have this identity transformation here? Maybe the compiler sees through this and optimises it, but if not, I'm thinking having to call transformation(unwrapped) on each channelRead could cause some unnecessary overhead when there's no actual transformation to be done.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this initially but this doesn't work because the compiler can't infer that if the transformation is none that InboundIn == ProducerElement. The compiler should be smart enough here though to see that the closure is a no-op in this case and be able to optimise it away.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no way the compiler makes that optimization, but it's ok, the identity function is going to be pretty cheap, and this is an implementation detail we can fix.

/// Bind the `ServerSocketChannel` to the `unixDomainSocketPath` parameter.
///
/// - Parameters:
/// - unixDomainSocketPath: The _Unix domain socket_ path to bind to. `unixDomainSocketPath` must not exist, it will be created by the system.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unixDomainSocketPath must not exist, it will be created by the system.

I don't think this applies to this version of the init, as you can set the cleanupExistingSocketFile flag to clean up the socket if it exists already, correct?

Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
Sources/NIOPosix/Bootstrap.swift Outdated Show resolved Hide resolved
synchronouslyWrapping channel: Channel,
backpressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark? = nil,
isOutboundHalfClosureEnabled: Bool = false,
protocolNegotiationClosure: @escaping (Channel) -> EventLoopFuture<Inbound>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit nervous about having these two methods differ only on the label of a trailing closure, which is commonly omitted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My plan was to keep these methods spi forever and never make them truly public since we only really use them from the bootstrap. Does that make sense to you and remove some of the nervousness?

) where InboundIn == ProducerElement {
self.eventLoop = eventLoop
self.closeRatchet = closeRatchet
self.transformation = .sync { $0 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no way the compiler makes that optimization, but it's ok, the identity function is going to be pretty cheap, and this is an implementation detail we can fix.

synchronouslyWrapping: serverChannel,
backpressureStrategy: serverBackpressureStrategy,
transformationClosure: { channel in
try NIOAsyncChannel(
Copy link
Contributor

@adam-fowler adam-fowler Apr 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears this is being called on the server channel eventloop, but it expects to be called on the child channel eventloop.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Good catch!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should run your tests with an EventLoopGroup with more than one thread or at least have one test with this. Three is a good number to ensure you don't have a server and a client interleaving EventLoop usage. I was running with two threads and only caught this because I had a test which had two client requests in it which opened two child channels

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very good piece of advice. I also recommend adding preconditionInEventLoop checks for any code that expects it, because they're fairly cheap and they flush out these bugs really early.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the preconditions were already in place and our tests caught this once I added more than on thread to the ELG. I also just pushed some commits that refactors the bootstrapping more to make all of this properly work.

# Motivation
In my previous PR, we added a new async bridge from a NIO `Channel` to Swift Concurrency primitives in the from of the `NIOAsyncChannel`. This type alone is already helpful in bridging `Channel`s to Concurrency; however, it is hard to use since it requires to wrap the `Channel` at the right time otherwise we will drop reads. Furthermore, in the case of protocol negotiation this becomes even trickier since we need to wait until it finishes and then wrap the `Channel`.

# Modification
This PR introduces a few things:
1. New methods on the `ServerBootstrap` which allow the creation of `NIOAsyncChannel` based channels. This can be used in all cases where no protocol negotiation is involved.
2. A new protocol and type called `NIOProtocolNegotiationHandler` and `NIOProtocolNegotiationResult` which is used to identify channel handlers that are doing protocol negotiation.
3. New methods on the `ServerBootstrap` that are aware of protocol negotiation.

# Result
We can now easily and safely create new `AsyncChannel`s from the `ServerBootstrap`
Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left some notes in the diff.

I also have a broader structural question I want us to think about: is searching for the handler by type the right thing to do here? This forces a lot of runtime type checking in more complex pipelines, which may not be the right thing to do.

Our other options are:

  1. Search by name. It's a little-known feature of NIO that handlers can be named, and can be searched for by name. This is a little cheaper, and might help us.
  2. Require that the handler be passed to the bind call. This is tricky, because most handlers aren't Sendable.
  3. Update the childChannelInitializer to require that the user returns the negotiation handler, instead of Void.

Do you have thoughts here?

}

private typealias MakeServerChannel = (_ eventLoop: SelectableEventLoop, _ childGroup: EventLoopGroup, _ enableMPTCP: Bool) throws -> ServerSocketChannel
private typealias Register = (EventLoop, ServerSocketChannel) -> EventLoopFuture<Void>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do either of these need @Sendable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes Register needs to Sendable since it is run on the server socket EL. However, MakeServerChannel is run inline and not escaping.

@FranzBusch
Copy link
Member Author

Our other options are:

  1. Search by name. It's a little-known feature of NIO that handlers can be named, and can be searched for by name. This is a little cheaper, and might help us.
  2. Require that the handler be passed to the bind call. This is tricky, because most handlers aren't Sendable.
  3. Update the childChannelInitializer to require that the user returns the negotiation handler, instead of Void.

Do you have thoughts here?

Good question!

  1. I don't think searching by name works here. The problem is that we need the fully typed generic handler. We often expect to have something like a NIOTypedApplicationProtocolNegotiationHandler<Something> and when we search by name we would still need to cast it. Potentially we can only do this dynamic casting once instead of for every handler.
  2. Yeah the not Sendability of handlers doesn't really make this possible. That's why I ruled this out earlier.
  3. I choose not to do this because it would require some larger changes in how we do the bootstrap since we have to tie together the return type of the childChannelInitializer to the type we expect to return from bind. We can only do this by introducing a new bootstrap type which I tried to avoid so far. However, we can definitely explore this and it would remove the runtime checks.

WDYT?

@Lukasa
Copy link
Contributor

Lukasa commented Apr 26, 2023

I wonder if it's a good time for us to explore (3). However, as this is SPI we can merge this without that change and investigate it later.

@FranzBusch
Copy link
Member Author

I wonder if it's a good time for us to explore (3). However, as this is SPI we can merge this without that change and investigate it later.

I would prefer to merge this now to unblock the HTTP2 work and will track this as an open item before we make the APIs public

@Lukasa Lukasa added the semver/none No version bump required. label Apr 26, 2023
@FranzBusch FranzBusch enabled auto-merge (squash) April 26, 2023 13:51
@FranzBusch FranzBusch merged commit d836d6b into apple:main Apr 26, 2023
@FranzBusch FranzBusch deleted the fb-async-channel-bootstrap branch April 26, 2023 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver/none No version bump required.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants