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

HTTP/2 negotiated connection pipeline config #408

Merged
merged 9 commits into from Jul 12, 2023

Conversation

rnro
Copy link
Contributor

@rnro rnro commented Jul 5, 2023

Motivation:

This continues the work to expose functionality which allows users to interact with HTTP/2 connections via async abstractions and using structured concurrency.

This work enables protocol negotiation between HTTP/1.1 and HTTP/2 in its most generic form.

Modifications:

Enable protocol negotiation between HTTP/1.1 and HTTP/2 using typed negotiation results (NIOProtocolNegotiationResult) from the NIOTypedApplicationProtocolNegotiationHandler.
In the HTTP/2 case the negotiation result will return the HTTP/2 connection channel and the NIOHTTP2Handler.AsyncStreamMultiplexer which exposes inbound streams as an iterable async stream.

Result:

Users will be able to set up negotiated HTTP/1.1 / HTTP/2 connections and iterate over inbound streams.

@rnro rnro requested review from glbrntt and FranzBusch July 5, 2023 12:58
@@ -380,6 +380,63 @@ extension Channel {
return self.pipeline.addHandler(alpnHandler)
}


/// Configures a channel to perform a HTTP/2 secure upgrade with typed negotiation results.
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT

Suggested change
/// Configures a channel to perform a HTTP/2 secure upgrade with typed negotiation results.
/// Configures a channel to perform an HTTP/2 secure upgrade with typed negotiation results.

/// the negotiation
@_spi(AsyncChannel)
public func configureHTTP2AsyncSecureUpgrade<HTTP1Output, HTTP2Output>(
h2ChannelConfigurator: @escaping (Channel) throws -> EventLoopFuture<HTTP2Output>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we call this http2ChannelConfigurator to align with the other parameter. I think these closures also need to be @Sendable.

Comment on lines 843 to 769
/// `NegotiationResult` is a generic negotiation result holder for HTTP/1.1 and HTTP/2
@_spi(AsyncChannel)
public enum NegotiationResult<HTTP1Output, HTTP2Output> {
case http1_1(HTTP1Output)
case http2(HTTP2Output)
}
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 we need to be a bit more specific here. Especially if we want to have something that does H1, H2, H3 in the future. What about naming this HTTP1And2NegotiationResult? Also open for better names cc @Lukasa @glbrntt

Copy link
Contributor

Choose a reason for hiding this comment

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

We need a NIO prefix at least. I'm not sure if we need to be quite so wary of H3 since it will be a new package/module and so won't need a NIO prefix.

How about NIONegotiatedHTTPVersion? Or NIOHTTPNegotiationResult? I prefer the former because it doesn't start with 8 capital letters...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've gone with NIONegotiatedHTTPVersion

) -> EventLoopFuture<Void> {
return self._commonHTTPServerPipeline(configurator: configurator, h2ConnectionChannelConfigurator: h2ConnectionChannelConfigurator) { channel in
channel.configureHTTP2Pipeline(
h1ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP1Output>,
Copy link
Contributor

Choose a reason for hiding this comment

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

This uses a type on the H2 handler which is a bit confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've created a pair of simplified global types which are used everywhere except for where the existing NIOHTTP2Handler.StreamInitializer was already used.

/// - h2ConnectionChannelConfigurator: An optional callback that will be invoked only
/// when the negotiated protocol is H2 to configure the connection channel.
/// when the negotiated protocol is HTTP/2 to configure the connection channel.
/// - inboundStreamInitializer: A closure that will be called whenever the remote peer initiates a new stream.
Copy link
Contributor

Choose a reason for hiding this comment

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

This doc comment doesn't match with the parameter names

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think they do but that the diff is presenting them in a confusing way

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I restructured the original commit to split out the simple moving of a function to group like functions. Hopefully that makes the main diff easier to understand

channel.configureHTTP2Pipeline(
h1ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP1Output>,
h2ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP2Output>,
streamInitializer: @escaping NIOHTTP2Handler.StreamInitializerWithOutput<HTTP2StreamOutput>
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 we should holistically prefix the parameters with either h1/2 or http1/http2 if they are for one specific protocol. I got quite confused trying to understand what parameter applies to what

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup, agree, streamInitializer is not obviously http2

h1ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP1Output>,
h2ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP2Output>,
streamInitializer: @escaping NIOHTTP2Handler.StreamInitializerWithOutput<HTTP2StreamOutput>
) throws -> EventLoopFuture<NIOProtocolNegotiationResult<NegotiationResult<HTTP1Output, (HTTP2Output, NIOHTTP2Handler.AsyncStreamMultiplexer<HTTP2StreamOutput>)>>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I am a bit surprised by this return type. I rather expected us to return the handler instead of the result here. The reason for this is that our bootstrap methods require the channel initialiser to return a Handler which we then use to await for the result.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've changed this to match the bootstrap methods.

@rnro rnro force-pushed the async_channels_protocol_negotiation branch from 5ac7dd4 to d3d63d6 Compare July 5, 2023 14:44
@@ -434,6 +491,47 @@ extension Channel {
}
}

/// Configures a `ChannelPipeline` to speak either HTTP or HTTP/2 according to what can be negotiated with the client.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Configures a `ChannelPipeline` to speak either HTTP or HTTP/2 according to what can be negotiated with the client.
/// Configures a `ChannelPipeline` to speak either HTTP/1.1 or HTTP/2 according to what can be negotiated with the client.

Sources/NIOHTTP2/HTTP2PipelineHelpers.swift Show resolved Hide resolved
channel.configureHTTP2Pipeline(
h1ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP1Output>,
h2ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP2Output>,
streamInitializer: @escaping NIOHTTP2Handler.StreamInitializerWithOutput<HTTP2StreamOutput>
Copy link
Contributor

Choose a reason for hiding this comment

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

Yup, agree, streamInitializer is not obviously http2

@rnro rnro requested review from glbrntt and FranzBusch July 6, 2023 08:12
@rnro rnro force-pushed the async_channels_protocol_negotiation branch from 36a06c1 to f6a2279 Compare July 6, 2023 09:40
/// is ready to negotiate. This can then be used to access the ``NIOProtocolNegotiationResult`` which may itself
/// be waited on to retrieve the result of the negotiation
@_spi(AsyncChannel)
public func configureHTTP2AsyncSecureUpgrade<HTTP1Output, HTTP2Output>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we intending to make this public in the end? I don't see a lot of usage for this without adding the protocol handlers in there so maybe we should make it non public in the end and right now add an _ to the method name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've made this internal for now. It was originally public in parity with the non-async function but we can see if there's a need to make it public.

Comment on lines 591 to 593
connectionConfiguration: NIOHTTP2Handler.ConnectionConfiguration,
streamConfiguration: NIOHTTP2Handler.StreamConfiguration,
streamDelegate: NIOHTTP2StreamDelegate? = nil,
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO we should prefix those methods with http2 as well. They only apply to it and right now you can only discover this by reading the docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Configuration has been largely refactored.

/// be waited on to retrieve the result of the negotiation
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@_spi(AsyncChannel)
public func configureAsyncHTTPServerPipeline<HTTP1Output, HTTP2Output, HTTP2StreamOutput>(
Copy link
Contributor

Choose a reason for hiding this comment

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

What about naming the HTTP2Ouput generic type as HTTP2ConnectionOutput to better differentiate the two H2 types. (Maybe also add it for the H1 type

channel.configureHTTP2Pipeline(
http1ChannelConfigurator: @escaping NIOChannelInitializerWithOutput<HTTP1Output>,
http2ChannelConfigurator: @escaping NIOChannelInitializerWithOutput<HTTP2Output>,
http2StreamInitializer: @escaping NIOChannelInitializerWithOutput<HTTP2StreamOutput>
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add inbound in this parameters name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Configuration has been largely refactored.

Comment on lines 492 to 504
class HTTP1ServerRequestRecorderHandler: ChannelInboundHandler {
typealias InboundIn = HTTPServerRequestPart

var receivedParts: [HTTPServerRequestPart] = []

func channelRead(context: ChannelHandlerContext, data: NIOAny) {
self.receivedParts.append(self.unwrapInboundIn(data))
context.fireChannelRead(data)
}
}

/// A simple channel handler that records inbound frames.
class HTTP1ClientResponseRecorderHandler: ChannelInboundHandler {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
class HTTP1ServerRequestRecorderHandler: ChannelInboundHandler {
typealias InboundIn = HTTPServerRequestPart
var receivedParts: [HTTPServerRequestPart] = []
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
self.receivedParts.append(self.unwrapInboundIn(data))
context.fireChannelRead(data)
}
}
/// A simple channel handler that records inbound frames.
class HTTP1ClientResponseRecorderHandler: ChannelInboundHandler {
final class HTTP1ServerRequestRecorderHandler: ChannelInboundHandler {
typealias InboundIn = HTTPServerRequestPart
var receivedParts: [HTTPServerRequestPart] = []
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
self.receivedParts.append(self.unwrapInboundIn(data))
context.fireChannelRead(data)
}
}
/// A simple channel handler that records inbound frames.
final class HTTP1ClientResponseRecorderHandler: ChannelInboundHandler {

Comment on lines 452 to 453
let clientRecorder = try await self.clientChannel.pipeline.handler(type: HTTP1ClientResponseRecorderHandler.self).get()
let serverRecorder = try await self.serverChannel.pipeline.handler(type: HTTP1ServerRequestRecorderHandler.self).get()
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 not safe. Those two types are not Sendable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've made these recorder types Sendable

@rnro rnro force-pushed the async_channels_protocol_negotiation branch from ee905c4 to b7da1fd Compare July 10, 2023 15:54
@rnro rnro requested a review from FranzBusch July 10, 2023 15:57
@rnro rnro force-pushed the async_channels_protocol_negotiation branch from b7da1fd to 8705984 Compare July 10, 2023 15:59
public var connection: ConnectionConfiguration = .init()
/// The settings that will be used when establishing new streams. These mainly pertain to flow control.
public var stream: StreamConfiguration = .init()
public init() {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Given we're just group two configs here I think we'll be okay having them in the init too.

Sources/NIOHTTP2/HTTP2PipelineHelpers.swift Show resolved Hide resolved
Sources/NIOHTTP2/HTTP2PipelineHelpers.swift Show resolved Hide resolved
/// be waited on to retrieve the result of the negotiation
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@_spi(AsyncChannel)
public func configureAsyncHTTPServerPipeline<HTTP1ConnectionOutput, HTTP2ConnectionOutput, HTTP2StreamOutput>(
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 the existing version of this has a notion of "upgrade" in its name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The method which mentions "upgrade" is one layer down, the analogue of this is configureCommonHTTPServerPipeline - but we don't have any common configuration because we don't translate H2 into H1 so that's not relevant.

@@ -673,55 +725,112 @@ extension ChannelPipeline.SynchronousOperations {
///
/// Using this rather than implementing a similar function yourself allows that pipeline to evolve without breaking your code.
///

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change

/// `HTTP2AsyncConfiguration` contains all configuration required for setting up an HTTP/2 async connection.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@_spi(AsyncChannel)
public struct HTTP2AsyncConfiguration<HTTP2ConnectionInbound, HTTP2ConnectionOutbound, HTTP2StreamInbound, HTTP2StreamOutbound> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public struct HTTP2AsyncConfiguration<HTTP2ConnectionInbound, HTTP2ConnectionOutbound, HTTP2StreamInbound, HTTP2StreamOutbound> {
public struct HTTP2AsyncConfiguration<HTTP2ConnectionInbound, HTTP2ConnectionOutbound, HTTP2StreamInbound, HTTP2StreamOutbound>: Sendable {

Comment on lines 824 to 827
inboundType: Inbound.Type = Inbound.self,
outboundType: Outbound.Type = Outbound.self,
backpressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark? = nil,
isOutboundHalfClosureEnabled: Bool? = nil
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 these were added in a different order in NIO: inboundType and outboundType came at the end

typealias OutboundOut = HTTPServerResponsePart

func channelRead(context: ChannelHandlerContext, data: NIOAny) {
guard case .end = self.unwrapInboundIn(data) else {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: prefer switching over all cases

typealias InboundIn = message

private let partsLock = NIOLock()
var _receivedParts: [message] = []
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var _receivedParts: [message] = []
private var _receivedParts: [message] = []

@@ -64,6 +64,8 @@ extension XCTestCase {

/// Have two `NIOAsyncTestingChannel` objects send and receive data from each other until
/// they make no forward progress.
///
/// ** This function is not thread safe and can lead to deadlocks, prefer the one-way variant which is less error-prone**
Copy link
Contributor

Choose a reason for hiding this comment

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

It is thread-safe, the problem is that calling it from multiple threads is racy.

@rnro rnro requested a review from glbrntt July 11, 2023 12:58
Copy link
Contributor

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

over to you @FranzBusch

@glbrntt
Copy link
Contributor

glbrntt commented Jul 11, 2023

oh, looks like you regressed allocations @rnro

/// `HTTP2AsyncConfiguration` contains all configuration required for setting up an HTTP/2 async connection.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@_spi(AsyncChannel)
public struct HTTP2AsyncConfiguration<HTTP2ConnectionInbound: Sendable, HTTP2ConnectionOutbound: Sendable, HTTP2StreamInbound: Sendable, HTTP2StreamOutbound: Sendable>: Sendable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public struct HTTP2AsyncConfiguration<HTTP2ConnectionInbound: Sendable, HTTP2ConnectionOutbound: Sendable, HTTP2StreamInbound: Sendable, HTTP2StreamOutbound: Sendable>: Sendable {
public struct NIOHTTP2AsyncConfiguration<HTTP2ConnectionInbound: Sendable, HTTP2ConnectionOutbound: Sendable, HTTP2StreamInbound: Sendable, HTTP2StreamOutbound: Sendable>: Sendable {

@rnro rnro requested a review from FranzBusch July 11, 2023 14:40
@rnro
Copy link
Contributor Author

rnro commented Jul 11, 2023

@swift-server-bot test this please

rnro added 8 commits July 11, 2023 16:49
Motivation:

This continues the work to expose functionality which allows users to interact
with HTTP/2 connections via async abstractions and using structured
concurrency.

This work enables protocol negotiation between HTTP/1.1 and HTTP/2 in its most generic form.

Modifications:

Enable protocol negotiation between HTTP/1.1 and HTTP/2 using typed
negotiation results (`NIOProtocolNegotiationResult`) from the
`NIOTypedApplicationProtocolNegotiationHandler`.
In the HTTP/2 case the negotiation result will return the HTTP/2 connection
channel and the `NIOHTTP2Handler.AsyncStreamMultiplexer` which exposes
inbound streams as an iterable async stream.

Result:

Users will be able to set up negotiated HTTP/1.1 / HTTP/2 connections and
iterate over inbound streams.
Large scale refactoring of code and function visibility to make the
various APIs currently under SPI more manageable.

As the API surface has become more complex with the addition of more
layers it became apparent that we needed to take steps to simplify.
@rnro rnro force-pushed the async_channels_protocol_negotiation branch from df3b529 to 97da616 Compare July 11, 2023 15:57
@rnro
Copy link
Contributor Author

rnro commented Jul 11, 2023

Rebased on top of #409

@rnro
Copy link
Contributor Author

rnro commented Jul 11, 2023

All of the API breakage is in code added since the last release. Almost all of it is Async... configuration functions, there are also some closure type aliases which they used but we've simplified and done away with.

#if swift(>=5.7)
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@_spi(AsyncChannel)
extension NIOHTTP2AsyncConfiguration: @unchecked Sendable {}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: add a comment as to why this is @unchecked

@glbrntt glbrntt merged commit 044339d into apple:main Jul 12, 2023
7 of 8 checks passed
@rnro rnro added the needs-no-version-bump For PRs that when merged do not need a bump in version number. label Oct 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-no-version-bump For PRs that when merged do not need a bump in version number.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants