Conversation
glbrntt
left a comment
There was a problem hiding this comment.
Could you add the unmodified code from gRPC as the first commit in this PR to make it easier to see the changes you've made?
edf43a7 to
ce91534
Compare
| /// While NIO's `EmbeddedEventLoop` provides control over its view of time (and therefore any | ||
| /// events scheduled on it) it doesn't offer a way to get the current time. This is usually done | ||
| /// via `NIODeadline`. | ||
| public struct Clock { |
There was a problem hiding this comment.
I would err on the side of caution and keep this internal/package because it was the minimal API required to enable various tests in gRPC. We can always think a bit harder about the API and make it public in the future if necessary.
| /// - keepaliveConfiguration: Configuration for keep-alive ping behavior. Defaults to `nil`, disabling keep-alive | ||
| /// pings. | ||
| /// - clock: A clock providing the current time. | ||
| public init( |
There was a problem hiding this comment.
I recommend adding a config struct for all of these values: it will make API evolution much easier (e.g. if more config is added later). Also means you can provide a set of sensible default values as a static computed property on that config too.
There was a problem hiding this comment.
Agreed. I've also added a default configuration property in 92aa96e:
public static var defaults: Self {
Self(
maxIdleTime: nil,
maxAge: nil,
maxGraceTime: .minutes(5),
keepalive: .init(pingInterval: .hours(2), ackTimeout: .seconds(20))
)
}| @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) | ||
| public func configureAsyncHTTP2Pipeline<Output: Sendable>( | ||
| mode: NIOHTTP2Handler.ParserMode, | ||
| connectionManager: NIOHTTP2ServerConnectionManagementHandler, |
There was a problem hiding this comment.
Passing in the handler is a bit odd. It'd be more idiomatic to pass in the config for the handler and have this func create the handler for the caller.
Sources/NIOHTTP2/Timer.swift
Outdated
| } | ||
| } | ||
|
|
||
| extension Timer: NIOScheduledCallbackHandler, @unchecked Sendable where Handler: Sendable { |
There was a problem hiding this comment.
This unchecked sendable seems wrong: it relies on correct use. Can we make it actually Sendable?
| } | ||
|
|
||
| /// A manual clock for testing that allows explicit control over time. | ||
| public final class Manual { |
There was a problem hiding this comment.
These publics don’t do anything, so we can safely remove them.
| } | ||
|
|
||
| public func handlerAdded(context: ChannelHandlerContext) { | ||
| assert(context.eventLoop === self.eventLoop) |
There was a problem hiding this comment.
Replace this assert with an in-event-loop assertion.
|
|
||
| // Timer handler views. | ||
| extension NIOHTTP2ServerConnectionManagementHandler { | ||
| struct MaxIdleTimerHandlerView: @unchecked Sendable, NIOScheduledCallbackHandler { |
There was a problem hiding this comment.
What’s the need for these to be Sendable? If we do actually require it, we should fully enforce it using proper preconditions or loop bounds.
There was a problem hiding this comment.
In Timer, we call the EventLoop/scheduleCallback(at:handler:) function with the handler argument set to the view.
The scheduleCallback function requires handler to be Sendable, which is why we need to make these views Sendable.
|
|
||
| extension NIOHTTP2ServerConnectionManagementHandler { | ||
| /// A delegate for receiving HTTP/2 stream lifecycle events. | ||
| public struct HTTP2StreamDelegate: @unchecked Sendable, NIOHTTP2StreamDelegate { |
There was a problem hiding this comment.
Why is this public? Same note here re sendability as well.
There was a problem hiding this comment.
NIOHTTP2ServerConnectionManagementHandler will only know when streams are opened / closed via its delegate; users need to pass this stream delegate to NIOHTTP2Handler for NIOHTTP2ServerConnectionManagementHandler to work as intended.
This reason this is public is so users can also use NIOHTTP2ServerConnectionManagementHandler without this pipeline setup helper.
| } | ||
|
|
||
| /// Configuration parameters for ``NIOHTTP2ServerConnectionManagementHandler``. | ||
| public struct Configuration: Sendable { |
There was a problem hiding this comment.
This is a sufficiently large range of configuration knobs that it might be nice for us to add some narrative documentation explaining what this thing is for, how it works, and when you might use it.
| /// - pingInterval: The amount of time to wait after reading data before sending a keep-alive ping. | ||
| /// - ackTimeout: The amount of time the client has to reply after the server sends a keep-alive ping to keep | ||
| /// the connection open. The connection is closed if no reply is received. | ||
| public init(pingInterval: TimeAmount, ackTimeout: TimeAmount = .seconds(20)) { |
There was a problem hiding this comment.
What's the principle behind defaulting only the second parameter?
There was a problem hiding this comment.
In gRPC-swift-nio-transport where this connection manager is derived from, both the pingInterval and ackTimeout arguments are contained in the top-level config type, not in a standalone Keepalive type like in this PR.
It is possible (in gRPC-swift-nio-transport) to set pingInterval to a non-nil value and ackTimeout to nil. In that case, a default of 20 seconds is used for ackTimeout.
I can remove the default argument for ackTimeout here since that case does not arise: users must define an ackTimeout when pingTimeout is non-nil.
Sources/NIOHTTP2/Timer.swift
Outdated
|
|
||
| /// Start or restart the timer. | ||
| mutating func start() { | ||
| self.eventLoop.assertInEventLoop() |
There was a problem hiding this comment.
These assertions are weird now that this type might be Sendable. Are they actually required in any way for correct function? It seems like probably not.
| context.fireUserInboundEventTriggered(event) | ||
| } | ||
|
|
||
| public func errorCaught(context: ChannelHandlerContext, error: any Error) { |
There was a problem hiding this comment.
Is it appropriate for us to suppress errors here?
There was a problem hiding this comment.
We should not be determining whether to close the connection here, and just be propagating the error down the pipeline.
I've removed the errorCaught function entirely.
| self.inReadLoop = false | ||
|
|
||
| // Done reading: schedule the keep-alive timer. | ||
| self.keepaliveTimerHandler?.start() |
There was a problem hiding this comment.
Is this logic entirely right? We can get a channelReadComplete with no actual read data getting to us here. Is the semantic that we need a read of anything from the network, even if it never produced a frame, or do we need a complete frame?
There was a problem hiding this comment.
I think any activity on the network originating from the client is sufficient for us to reset the keepalive timer.
@glbrntt, what are your thoughts on this? The gRPC proposal, which I believe the connection manager implementation in gRPC-swift-nio-transport closely follows, states this:
"keepalive time is ideally measured from the time of the last byte read"
Note: The above is actually from the client-side connection management proposal (the server-side proposal refers readers to the client-side proposal for implementing keepalive checks (see this)).
There was a problem hiding this comment.
We don't need a complete frame, just any bytes on the network if we're following the gRPC semantics.
| // Timer handler views. | ||
| extension NIOHTTP2ServerConnectionManagementHandler { | ||
| struct MaxIdleTimerHandlerView: Sendable, NIOScheduledCallbackHandler { | ||
| private let handler: NIOLoopBound<NIOHTTP2ServerConnectionManagementHandler> |
There was a problem hiding this comment.
It's still not clear to me why these views need to be Sendable. There's no requirement for it that I can see.
There was a problem hiding this comment.
Sorry, I forgot to answer this in your previous comment. See #532 (comment).
… errors down the pipeline.
| /// Send a GOAWAY frame with the code "enhance your calm" and immediately close the connection. | ||
| case enhanceYourCalmThenClose(HTTP2StreamID) |
There was a problem hiding this comment.
This is never used because the keepalive policing was removed
| /// Creates a clock with a manual time source for testing. | ||
| /// - Parameter time: The manual time source. | ||
| /// - Returns: A clock that uses the provided manual time source. | ||
| static func manual(_ time: Manual) -> Self { | ||
| Self(base: .manual(time)) | ||
| } | ||
|
|
||
| /// A manual clock for testing that allows explicit control over time. | ||
| final class Manual { | ||
| private(set) var time: NIODeadline | ||
|
|
||
| /// Creates a manual clock with time starting at zero. | ||
| init() { | ||
| self.time = .uptimeNanoseconds(0) | ||
| } | ||
|
|
||
| func advance(by amount: TimeAmount) { | ||
| self.time = self.time + amount | ||
| } | ||
| } |
There was a problem hiding this comment.
Looks like this is never used, even by the tests.
glbrntt
left a comment
There was a problem hiding this comment.
LGTM although it'd be good to add the note about why we can ignore pings (but not ping acks)
| self.handlePingAck(context: context, data: data) | ||
|
|
||
| default: | ||
| () // Only interested in PING frames with the ACK flag set, ignore the rest. |
There was a problem hiding this comment.
It's worth adding a note here that the HTTP2Handler acks PING frames and that's why we can ignore them here.
Motivation:
NIOHTTP2Handlercurrently does not support graceful shutdown because it does not action upon aChannelShouldQuiesceEventevent. See issue #336.Per the comment on that issue,
gRPC-swift-nio-transportimplements this functionality in a separate channel handler, namedServerConnectionManagementHandler. We should introduce this to this repo too.Modifications:
ServerConnectionManagementHandlerfromgRPC-swift-nio-transportwith some slight modifications such as removing gRPC-specific functionality like policing client pings.configureAsyncHTTP2Pipelinehelper, which takes the connection manager handler as an argument and sets it up alongsideNIOHTTP2Handler.Result:
Addresses issue #336.