Add an HTTP/1.1 connection pool (#105)

motivation: Better performance thanks to connection reuse

changes:
- Added a connection pool for HTTP/1.1
- All requests automatically use the connection pool
- Up to 8 parallel connections per (scheme, host, port)
- Multiple additional unit tests
This commit is contained in:
Trevör
2020-02-25 15:43:16 +00:00
committed by GitHub
parent de7421906c
commit 19e2ea727e
10 changed files with 1726 additions and 208 deletions
@@ -192,7 +192,10 @@ internal final class HTTPBin {
compress: Bool = false,
bindTarget: BindTarget = .localhostIPv4RandomPort,
simulateProxy: HTTPProxySimulator.Option? = nil,
channelPromise: EventLoopPromise<Channel>? = nil) {
channelPromise: EventLoopPromise<Channel>? = nil,
connectionDelay: TimeAmount = .seconds(0),
maxChannelAge: TimeAmount? = nil,
refusesConnections: Bool = false) {
let socketAddress: SocketAddress
switch bindTarget {
case .localhostIPv4RandomPort:
@@ -200,12 +203,16 @@ internal final class HTTPBin {
case .unixDomainSocket(let path):
socketAddress = try! SocketAddress(unixDomainSocketPath: path)
}
self.serverChannel = try! ServerBootstrap(group: self.group)
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
.childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
.childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: true, withErrorHandling: true)
.flatMap {
guard !refusesConnections else {
return channel.eventLoop.makeFailedFuture(HTTPBinError.refusedConnection)
}
return channel.eventLoop.scheduleTask(in: connectionDelay) {}.futureResult.flatMap {
channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: true, withErrorHandling: true).flatMap {
if compress {
return channel.pipeline.addHandler(HTTPResponseCompressor())
} else {
@@ -221,8 +228,7 @@ internal final class HTTPBin {
} else {
return channel.eventLoop.makeSucceededFuture(())
}
}
.flatMap {
}.flatMap {
if ssl {
return HTTPBin.configureTLS(channel: channel).flatMap {
channel.pipeline.addHandler(HttpBinHandler(channelPromise: channelPromise))
@@ -231,8 +237,8 @@ internal final class HTTPBin {
return channel.pipeline.addHandler(HttpBinHandler(channelPromise: channelPromise))
}
}
}
.bind(to: socketAddress).wait()
}
}.bind(to: socketAddress).wait()
}
func shutdown() throws {
@@ -245,6 +251,10 @@ internal final class HTTPBin {
}
}
enum HTTPBinError: Error {
case refusedConnection
}
final class HTTPProxySimulator: ChannelInboundHandler, RemovableChannelHandler {
typealias InboundIn = HTTPServerRequestPart
typealias InboundOut = HTTPServerResponsePart
@@ -327,14 +337,53 @@ internal final class HttpBinHandler: ChannelInboundHandler {
let channelPromise: EventLoopPromise<Channel>?
var resps = CircularBuffer<HTTPResponseBuilder>()
var closeAfterResponse = false
var delay: TimeAmount = .seconds(0)
let creationDate = Date()
let maxChannelAge: TimeAmount?
var shouldClose = false
var isServingRequest = false
init(channelPromise: EventLoopPromise<Channel>? = nil) {
init(channelPromise: EventLoopPromise<Channel>? = nil, maxChannelAge: TimeAmount? = nil) {
self.channelPromise = channelPromise
self.maxChannelAge = maxChannelAge
}
func handlerAdded(context: ChannelHandlerContext) {
if let maxChannelAge = self.maxChannelAge {
context.eventLoop.scheduleTask(in: maxChannelAge) {
if !self.isServingRequest {
context.close(promise: nil)
} else {
self.shouldClose = true
}
}
}
}
func parseAndSetOptions(from head: HTTPRequestHead) {
if let delay = head.headers["X-internal-delay"].first {
if let milliseconds = Int64(delay) {
self.delay = TimeAmount.milliseconds(milliseconds)
} else {
assertionFailure("Invalid interval format")
}
} else {
self.delay = .nanoseconds(0)
}
if let connection = head.headers["Connection"].first {
self.closeAfterResponse = (connection == "close")
} else {
self.closeAfterResponse = false
}
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
self.isServingRequest = true
switch self.unwrapInboundIn(data) {
case .head(let req):
self.parseAndSetOptions(from: req)
let url = URL(string: req.uri)!
switch url.path {
case "/":
@@ -454,7 +503,19 @@ internal final class HttpBinHandler: ChannelInboundHandler {
responseBody.writeBytes(serialized)
context.write(wrapOutboundOut(.body(.byteBuffer(responseBody))), promise: nil)
}
context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
context.eventLoop.scheduleTask(in: self.delay) {
context.writeAndFlush(self.wrapOutboundOut(.end(nil))).whenComplete { result in
self.isServingRequest = false
switch result {
case .success:
if self.closeAfterResponse || self.shouldClose {
context.close(promise: nil)
}
case .failure(let error):
assertionFailure("\(error)")
}
}
}
}
}
@@ -587,6 +648,29 @@ extension ByteBuffer {
}
}
struct EventLoopFutureTimeoutError: Error {}
extension EventLoopFuture {
func timeout(after failDelay: TimeAmount) -> EventLoopFuture<Value> {
let promise = self.eventLoop.makePromise(of: Value.self)
self.whenComplete { result in
switch result {
case .success(let value):
promise.succeed(value)
case .failure(let error):
promise.fail(error)
}
}
self.eventLoop.scheduleTask(in: failDelay) {
promise.fail(EventLoopFutureTimeoutError())
}
return promise.futureResult
}
}
private let cert = """
-----BEGIN CERTIFICATE-----
MIICmDCCAYACCQCPC8JDqMh1zzANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJ1