Skip to content

Commit c256b4a

Browse files
glbrnttMrMage
authored andcommitted
Delay marking connection as .ready until receiving initial settings frame (#529)
Motivation: It is possible for a connection to be immediately closed by the remote after a successful TLS handshake. In this case, the connection would be marked as ready and any backoff would be reset. That is, a reconnection attempt would be made immediately and the cycle would repeat. The gRPC core lib suggests resetting backoff after the initial settings frame has been made. Modifications: Add a handler which on observing the initial settings frame: 1. marks the connectivity as ready and, 2. removes itself from the pipeline. The client connection diagram was also updated to reflect this. Result: The connectivity state will only be marked as ready once the settings frame has been received from the peer.
1 parent c78d228 commit c256b4a

File tree

2 files changed

+80
-16
lines changed

2 files changed

+80
-16
lines changed

Sources/GRPC/ClientConnection.swift

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,20 @@ import Logging
2727
/// The connection is initially setup with a handler to verify that TLS was established
2828
/// successfully (assuming TLS is being used).
2929
///
30-
/// ▲ |
31-
/// HTTP2Frame│ │HTTP2Frame
32-
/// ┌─┴───────────────────────▼─┐
30+
/// ┌──────────────────────────┐
31+
/// │ DelegatingErrorHandler │
32+
/// └──────────▲───────────────┘
33+
/// HTTP2Frame│
34+
/// ┌──────────┴───────────────┐
35+
/// │ SettingsObservingHandler │
36+
/// └──────────▲───────────────┘
37+
/// HTTP2Frame│
38+
/// │ ⠇ ⠇ ⠇ ⠇
39+
/// │ ┌┴─▼┐ ┌┴─▼┐
40+
/// │ │ | │ | HTTP/2 streams
41+
/// │ └▲─┬┘ └▲─┬┘
42+
/// │ │ │ │ │ HTTP2Frame
43+
/// ┌─┴────── ─────────┴─▼───┴─▼┐
3344
/// │ HTTP2StreamMultiplexer |
3445
/// └─▲───────────────────────┬─┘
3546
/// HTTP2Frame│ │HTTP2Frame
@@ -49,11 +60,13 @@ import Logging
4960
///
5061
/// The `TLSVerificationHandler` observes the outcome of the SSL handshake and determines
5162
/// whether a `ClientConnection` should be returned to the user. In either eventuality, the
52-
/// handler removes itself from the pipeline once TLS has been verified. There is also a delegated
53-
/// error handler after the `HTTPStreamMultiplexer` in the main channel which uses the error
54-
/// delegate associated with this connection (see `DelegatingErrorHandler`).
63+
/// handler removes itself from the pipeline once TLS has been verified. There is also a handler
64+
/// after the multiplexer for observing the initial settings frame, after which it determines that
65+
/// the connection state is `.ready` and removes itself from the channel. Finally there is a
66+
/// delegated error handler which uses the error delegate associated with this connection
67+
/// (see `DelegatingErrorHandler`).
5568
///
56-
/// See `BaseClientCall` for a description of the remainder of the client pipeline.
69+
/// See `BaseClientCall` for a description of the pipelines assoicated with each HTTP/2 stream.
5770
public class ClientConnection {
5871
internal let logger: Logger
5972
/// The UUID of this connection, used for logging.
@@ -144,6 +157,7 @@ public class ClientConnection {
144157
channel,
145158
tls: configuration.tls?.configuration,
146159
serverHostname: configuration.tls?.hostnameOverride ?? configuration.target.host,
160+
connectivityMonitor: self.connectivity,
147161
errorDelegate: configuration.errorDelegate
148162
).flatMap {
149163
channel.connect(to: socketAddress)
@@ -224,14 +238,8 @@ extension ClientConnection {
224238
///
225239
/// - Parameter channel: The channel that was set.
226240
private func didSetChannel(to channel: EventLoopFuture<Channel>) {
227-
channel.whenComplete { result in
228-
switch result {
229-
case .success:
230-
self.connectivity.state = .ready
231-
232-
case .failure:
233-
self.connectivity.state = .shutdown
234-
}
241+
channel.whenFailure { _ in
242+
self.connectivity.state = .shutdown
235243
}
236244
}
237245

@@ -260,6 +268,7 @@ extension ClientConnection {
260268
configuration: configuration,
261269
group: configuration.eventLoopGroup,
262270
timeout: timeoutAndBackoff?.timeout,
271+
connectivityMonitor: connectivity,
263272
logger: logger
264273
)
265274

@@ -327,10 +336,12 @@ extension ClientConnection {
327336
/// - Parameter configuration: The configuration to prepare the bootstrap with.
328337
/// - Parameter group: The `EventLoopGroup` to use for the bootstrap.
329338
/// - Parameter timeout: The connection timeout in seconds.
339+
/// - Parameter connectivityMonitor: The connectivity state monitor for the created channel.
330340
private class func makeBootstrap(
331341
configuration: Configuration,
332342
group: EventLoopGroup,
333343
timeout: TimeInterval?,
344+
connectivityMonitor: ConnectivityStateMonitor,
334345
logger: Logger
335346
) -> ClientBootstrapProtocol {
336347
// Provide a server hostname if we're using TLS. Prefer the override.
@@ -354,6 +365,7 @@ extension ClientConnection {
354365
channel,
355366
tls: configuration.tls?.configuration,
356367
serverHostname: serverHostname,
368+
connectivityMonitor: connectivityMonitor,
357369
errorDelegate: configuration.errorDelegate
358370
)
359371
}
@@ -376,6 +388,7 @@ extension ClientConnection {
376388
_ channel: Channel,
377389
tls: TLSConfiguration?,
378390
serverHostname: String?,
391+
connectivityMonitor: ConnectivityStateMonitor,
379392
errorDelegate: ClientErrorDelegate?
380393
) -> EventLoopFuture<Void> {
381394
let tlsConfigured = tls.map {
@@ -385,8 +398,9 @@ extension ClientConnection {
385398
return (tlsConfigured ?? channel.eventLoop.makeSucceededFuture(())).flatMap {
386399
channel.configureHTTP2Pipeline(mode: .client)
387400
}.flatMap { _ in
401+
let settingsObserver = InitialSettingsObservingHandler(connectivityStateMonitor: connectivityMonitor)
388402
let errorHandler = DelegatingErrorHandler(delegate: errorDelegate)
389-
return channel.pipeline.addHandler(errorHandler)
403+
return channel.pipeline.addHandlers(settingsObserver, errorHandler)
390404
}
391405
}
392406
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2019, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import Foundation
17+
import NIO
18+
import NIOHTTP2
19+
import Logging
20+
21+
/// The purpose of this channel handler is to observe the initial settings frame on the root stream.
22+
/// This is an indication that the connection has become `.ready`. When this happens this handler
23+
/// will remove itself from the pipeline.
24+
class InitialSettingsObservingHandler: ChannelInboundHandler, RemovableChannelHandler {
25+
typealias InboundIn = HTTP2Frame
26+
typealias InboundOut = HTTP2Frame
27+
28+
private let connectivityStateMonitor: ConnectivityStateMonitor
29+
private let logger = Logger(subsystem: .clientChannel)
30+
31+
init(connectivityStateMonitor: ConnectivityStateMonitor) {
32+
self.connectivityStateMonitor = connectivityStateMonitor
33+
}
34+
35+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
36+
let frame = self.unwrapInboundIn(data)
37+
38+
if frame.streamID == .rootStream, case .settings(.settings) = frame.payload {
39+
self.logger.info("observed initial settings frame on the root stream")
40+
self.connectivityStateMonitor.state = .ready
41+
42+
// We're no longer needed at this point, remove ourselves from the pipeline.
43+
self.logger.debug("removing 'InitialSettingsObservingHandler' from the channel")
44+
context.pipeline.removeHandler(self, promise: nil)
45+
}
46+
47+
// We should always forward the frame.
48+
context.fireChannelRead(data)
49+
}
50+
}

0 commit comments

Comments
 (0)