Skip to content

Commit ae5f185

Browse files
authored
Use synchronous pipeline hops to remove windows. (#346)
Motivation: There is an awkward timing window in the TLSEventsHandler flow where it is possible for the NIOSSLClientHandler to fail the handshake on handlerAdded. If this happens, the TLSEventsHandler will not be in the pipeline, and so the handshake failure error will be lost and we'll get a generic one instead. This window can be resolved without performance penalty if we use the new synchronous pipeline operations view to add the two handlers backwards. If this is done then we can ensure that the TLSEventsHandler is always in the pipeline before the NIOSSLClientHandler, and so there is no risk of event loss. While I'm here, AHC does a lot of pipeline modification. This has led to lengthy future chains with lots of event loop hops for no particularly good reason. I've therefore replaced all pipeline operations with their synchronous counterparts. All but one sequence was happening on the correct event loop, and for the one that may not I've added a fast-path dispatch that should tolerate being on the wrong one. The result is cleaner, more linear code that also reduces the allocations and event loop hops. Modifications: - Use synchronous pipeline operations everywhere - Change the order of adding TLSEventsHandler and NIOSSLClientHandler Result: Faster, safer, fewer timing windows.
1 parent 0dda95c commit ae5f185

File tree

3 files changed

+60
-43
lines changed

3 files changed

+60
-43
lines changed

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let package = Package(
2121
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]),
2222
],
2323
dependencies: [
24-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.19.0"),
24+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.27.0"),
2525
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.8.0"),
2626
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.3.0"),
2727
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.5.1"),

Sources/AsyncHTTPClient/HTTPClient.swift

+40-24
Original file line numberDiff line numberDiff line change
@@ -561,16 +561,21 @@ public class HTTPClient {
561561
"ahc-task-el": "\(taskEL)"])
562562

563563
let channel = connection.channel
564-
let future: EventLoopFuture<Void>
565-
if let timeout = self.resolve(timeout: self.configuration.timeout.read, deadline: deadline) {
566-
future = channel.pipeline.addHandler(IdleStateHandler(readTimeout: timeout))
567-
} else {
568-
future = channel.eventLoop.makeSucceededFuture(())
569-
}
570564

571-
return future.flatMap {
572-
return channel.pipeline.addHandler(taskHandler)
573-
}.flatMap {
565+
func prepareChannelForTask0() -> EventLoopFuture<Void> {
566+
do {
567+
let syncPipelineOperations = channel.pipeline.syncOperations
568+
569+
if let timeout = self.resolve(timeout: self.configuration.timeout.read, deadline: deadline) {
570+
try syncPipelineOperations.addHandler(IdleStateHandler(readTimeout: timeout))
571+
}
572+
573+
try syncPipelineOperations.addHandler(taskHandler)
574+
} catch {
575+
connection.release(closing: true, logger: logger)
576+
return channel.eventLoop.makeFailedFuture(error)
577+
}
578+
574579
task.setConnection(connection)
575580

576581
let isCancelled = task.lock.withLock {
@@ -581,14 +586,19 @@ public class HTTPClient {
581586
return channel.writeAndFlush(request).flatMapError { _ in
582587
// At this point the `TaskHandler` will already be present
583588
// to handle the failure and pass it to the `promise`
584-
channel.eventLoop.makeSucceededFuture(())
589+
channel.eventLoop.makeSucceededVoidFuture()
585590
}
586591
} else {
587-
return channel.eventLoop.makeSucceededFuture(())
592+
return channel.eventLoop.makeSucceededVoidFuture()
593+
}
594+
}
595+
596+
if channel.eventLoop.inEventLoop {
597+
return prepareChannelForTask0()
598+
} else {
599+
return channel.eventLoop.flatSubmit {
600+
return prepareChannelForTask0()
588601
}
589-
}.flatMapError { error in
590-
connection.release(closing: true, logger: logger)
591-
return channel.eventLoop.makeFailedFuture(error)
592602
}
593603
}.always { _ in
594604
setupComplete.succeed(())
@@ -873,7 +883,7 @@ extension HTTPClient.Configuration {
873883
}
874884

875885
extension ChannelPipeline {
876-
func addProxyHandler(host: String, port: Int, authorization: HTTPClient.Authorization?) -> EventLoopFuture<Void> {
886+
func syncAddProxyHandler(host: String, port: Int, authorization: HTTPClient.Authorization?) throws {
877887
let encoder = HTTPRequestEncoder()
878888
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes))
879889
let handler = HTTPClientProxyHandler(host: host, port: port, authorization: authorization) { channel in
@@ -883,28 +893,34 @@ extension ChannelPipeline {
883893
channel.pipeline.removeHandler(decoder)
884894
}
885895
}
886-
return addHandlers([encoder, decoder, handler])
896+
897+
let sync = self.syncOperations
898+
try sync.addHandler(encoder)
899+
try sync.addHandler(decoder)
900+
try sync.addHandler(handler)
887901
}
888902

889-
func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, addSSLClient: Bool, handshakePromise: EventLoopPromise<Void>) {
903+
func syncAddSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, addSSLClient: Bool, handshakePromise: EventLoopPromise<Void>) {
890904
guard key.scheme.requiresTLS else {
891905
handshakePromise.succeed(())
892906
return
893907
}
894908

895909
do {
896-
let handlers: [ChannelHandler]
910+
let synchronousPipelineView = self.syncOperations
911+
912+
// We add the TLSEventsHandler first so that it's always in the pipeline before any other TLS handler we add.
913+
let eventsHandler = TLSEventsHandler(completionPromise: handshakePromise)
914+
try synchronousPipelineView.addHandler(eventsHandler)
915+
897916
if addSSLClient {
898917
let tlsConfiguration = tlsConfiguration ?? TLSConfiguration.forClient()
899918
let context = try NIOSSLContext(configuration: tlsConfiguration)
900-
handlers = [
919+
try synchronousPipelineView.addHandler(
901920
try NIOSSLClientHandler(context: context, serverHostname: (key.host.isIPAddress || key.host.isEmpty) ? nil : key.host),
902-
TLSEventsHandler(completionPromise: handshakePromise),
903-
]
904-
} else {
905-
handlers = [TLSEventsHandler(completionPromise: handshakePromise)]
921+
position: .before(eventsHandler)
922+
)
906923
}
907-
self.addHandlers(handlers).cascadeFailure(to: handshakePromise)
908924
} catch {
909925
handshakePromise.fail(error)
910926
}

Sources/AsyncHTTPClient/Utils.swift

+19-18
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,14 @@ extension NIOClientTCPBootstrap {
128128
return try self.makeBootstrap(on: eventLoop, host: host, requiresTLS: requiresTLS, configuration: configuration)
129129
.channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1)
130130
.channelInitializer { channel in
131-
let channelAddedFuture: EventLoopFuture<Void>
132-
switch configuration.proxy {
133-
case .none:
134-
channelAddedFuture = eventLoop.makeSucceededFuture(())
135-
case .some:
136-
channelAddedFuture = channel.pipeline.addProxyHandler(host: host, port: port, authorization: configuration.proxy?.authorization)
131+
do {
132+
if let proxy = configuration.proxy {
133+
try channel.pipeline.syncAddProxyHandler(host: host, port: port, authorization: proxy.authorization)
134+
}
135+
return channel.eventLoop.makeSucceededVoidFuture()
136+
} catch {
137+
return channel.eventLoop.makeFailedFuture(error)
137138
}
138-
return channelAddedFuture
139139
}
140140
}
141141

@@ -165,27 +165,28 @@ extension NIOClientTCPBootstrap {
165165
let requiresSSLHandler = configuration.proxy != nil && key.scheme.requiresTLS
166166
let handshakePromise = channel.eventLoop.makePromise(of: Void.self)
167167

168-
channel.pipeline.addSSLHandlerIfNeeded(for: key, tlsConfiguration: configuration.tlsConfiguration, addSSLClient: requiresSSLHandler, handshakePromise: handshakePromise)
168+
channel.pipeline.syncAddSSLHandlerIfNeeded(for: key, tlsConfiguration: configuration.tlsConfiguration, addSSLClient: requiresSSLHandler, handshakePromise: handshakePromise)
169+
170+
return handshakePromise.futureResult.flatMapThrowing {
171+
let syncOperations = channel.pipeline.syncOperations
172+
173+
try syncOperations.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
169174

170-
return handshakePromise.futureResult.flatMap {
171-
channel.pipeline.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
172-
}.flatMap {
173175
#if canImport(Network)
174176
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
175-
return channel.pipeline.addHandler(HTTPClient.NWErrorHandler(), position: .first)
177+
try syncOperations.addHandler(HTTPClient.NWErrorHandler(), position: .first)
176178
}
177179
#endif
178-
return channel.eventLoop.makeSucceededFuture(())
179-
}.flatMap {
180+
180181
switch configuration.decompression {
181182
case .disabled:
182-
return channel.eventLoop.makeSucceededFuture(())
183+
()
183184
case .enabled(let limit):
184185
let decompressHandler = NIOHTTPResponseDecompressor(limit: limit)
185-
return channel.pipeline.addHandler(decompressHandler)
186+
try syncOperations.addHandler(decompressHandler)
186187
}
187-
}.map {
188-
channel
188+
189+
return channel
189190
}
190191
}.flatMapError { error in
191192
#if canImport(Network)

0 commit comments

Comments
 (0)