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
Conversation
@@ -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. |
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.
NIT
/// 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>, |
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.
Can we call this http2ChannelConfigurator
to align with the other parameter. I think these closures also need to be @Sendable
.
/// `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) | ||
} |
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.
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 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...
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've gone with NIONegotiatedHTTPVersion
) -> EventLoopFuture<Void> { | ||
return self._commonHTTPServerPipeline(configurator: configurator, h2ConnectionChannelConfigurator: h2ConnectionChannelConfigurator) { channel in | ||
channel.configureHTTP2Pipeline( | ||
h1ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP1Output>, |
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 uses a type on the H2 handler which is a bit confusing.
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'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. |
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 doc comment doesn't match with the parameter names
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 they do but that the diff is presenting them in a confusing way
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 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> |
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 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
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.
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>)>>> { |
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 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.
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've changed this to match the bootstrap methods.
5ac7dd4
to
d3d63d6
Compare
@@ -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. |
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.
/// 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. |
channel.configureHTTP2Pipeline( | ||
h1ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP1Output>, | ||
h2ConnectionChannelConfigurator: @escaping NIOHTTP2Handler.ConnectionInitializerWithOutput<HTTP2Output>, | ||
streamInitializer: @escaping NIOHTTP2Handler.StreamInitializerWithOutput<HTTP2StreamOutput> |
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.
Yup, agree, streamInitializer
is not obviously http2
36a06c1
to
f6a2279
Compare
/// 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>( |
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.
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?
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'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.
connectionConfiguration: NIOHTTP2Handler.ConnectionConfiguration, | ||
streamConfiguration: NIOHTTP2Handler.StreamConfiguration, | ||
streamDelegate: NIOHTTP2StreamDelegate? = nil, |
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.
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.
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.
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>( |
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 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> |
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 we add inbound
in this parameters name?
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.
Configuration has been largely refactored.
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 { |
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.
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 { |
let clientRecorder = try await self.clientChannel.pipeline.handler(type: HTTP1ClientResponseRecorderHandler.self).get() | ||
let serverRecorder = try await self.serverChannel.pipeline.handler(type: HTTP1ServerRequestRecorderHandler.self).get() |
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 not safe. Those two types are not Sendable
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've made these recorder types Sendable
ee905c4
to
b7da1fd
Compare
b7da1fd
to
8705984
Compare
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() {} |
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.
Given we're just group two configs here I think we'll be okay having them in the init
too.
/// 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>( |
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 the existing version of this has a notion of "upgrade" in its name
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.
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. | |||
/// | |||
|
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.
/// `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> { |
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.
public struct HTTP2AsyncConfiguration<HTTP2ConnectionInbound, HTTP2ConnectionOutbound, HTTP2StreamInbound, HTTP2StreamOutbound> { | |
public struct HTTP2AsyncConfiguration<HTTP2ConnectionInbound, HTTP2ConnectionOutbound, HTTP2StreamInbound, HTTP2StreamOutbound>: Sendable { |
inboundType: Inbound.Type = Inbound.self, | ||
outboundType: Outbound.Type = Outbound.self, | ||
backpressureStrategy: NIOAsyncSequenceProducerBackPressureStrategies.HighLowWatermark? = nil, | ||
isOutboundHalfClosureEnabled: Bool? = nil |
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 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 { |
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.
nit: prefer switching over all cases
typealias InboundIn = message | ||
|
||
private let partsLock = NIOLock() | ||
var _receivedParts: [message] = [] |
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.
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** |
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.
It is thread-safe, the problem is that calling it from multiple threads is racy.
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.
over to you @FranzBusch
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 { |
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.
public struct HTTP2AsyncConfiguration<HTTP2ConnectionInbound: Sendable, HTTP2ConnectionOutbound: Sendable, HTTP2StreamInbound: Sendable, HTTP2StreamOutbound: Sendable>: Sendable { | |
public struct NIOHTTP2AsyncConfiguration<HTTP2ConnectionInbound: Sendable, HTTP2ConnectionOutbound: Sendable, HTTP2StreamInbound: Sendable, HTTP2StreamOutbound: Sendable>: Sendable { |
@swift-server-bot test this please |
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.
df3b529
to
97da616
Compare
Rebased on top of #409 |
All of the API breakage is in code added since the last release. Almost all of it is |
#if swift(>=5.7) | ||
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) | ||
@_spi(AsyncChannel) | ||
extension NIOHTTP2AsyncConfiguration: @unchecked Sendable {} |
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.
nit: add a comment as to why this is @unchecked
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 theNIOTypedApplicationProtocolNegotiationHandler
.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.