mirror of
https://github.com/swift-server/async-http-client.git
synced 2026-06-02 07:37:34 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user