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:
@@ -0,0 +1,653 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This source file is part of the AsyncHTTPClient open source project
|
||||
//
|
||||
// Copyright (c) 2019-2020 Apple Inc. and the AsyncHTTPClient project authors
|
||||
// Licensed under Apache License v2.0
|
||||
//
|
||||
// See LICENSE.txt for license information
|
||||
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import NIO
|
||||
import NIOConcurrencyHelpers
|
||||
import NIOHTTP1
|
||||
import NIOTLS
|
||||
|
||||
/// A connection pool that manages and creates new connections to hosts respecting the specified preferences
|
||||
///
|
||||
/// - Note: All `internal` methods of this class are thread safe
|
||||
final class ConnectionPool {
|
||||
/// The configuration used to bootstrap new HTTP connections
|
||||
private let configuration: HTTPClient.Configuration
|
||||
|
||||
/// The main data structure used by the `ConnectionPool` to retreive and create connections associated
|
||||
/// to a given `Key` .
|
||||
/// - Warning: This property should be accessed with proper synchronization, see `connectionProvidersLock`
|
||||
private var connectionProviders: [Key: HTTP1ConnectionProvider] = [:]
|
||||
|
||||
/// The lock used by the connection pool used to ensure correct synchronization of accesses to `_connectionProviders`
|
||||
///
|
||||
///
|
||||
/// - Warning: This lock should always be acquired *before* `HTTP1ConnectionProvider`s `stateLock` if used in combination with it.
|
||||
private let connectionProvidersLock = Lock()
|
||||
|
||||
init(configuration: HTTPClient.Configuration) {
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
||||
/// Gets the `EventLoop` associated with the given `Key` if it exists
|
||||
///
|
||||
/// This is part of an optimization used by the `.execute(...)` method when
|
||||
/// a request has its `EventLoopPreference` property set to `.indifferent`.
|
||||
/// Having a default `EventLoop` shared by the *channel* and the *delegate* avoids
|
||||
/// loss of performance due to `EventLoop` hopping
|
||||
func associatedEventLoop(for key: Key) -> EventLoop? {
|
||||
return self.connectionProvidersLock.withLock {
|
||||
self.connectionProviders[key]?.eventLoop
|
||||
}
|
||||
}
|
||||
|
||||
/// This method asks the pool for a connection usable by the specified `request`, respecting the specified options.
|
||||
///
|
||||
/// - parameter request: The request that needs a `Connection`
|
||||
/// - parameter preference: The `EventLoopPreference` the connection pool will respect to lease a new connection
|
||||
/// - parameter deadline: The connection timeout
|
||||
/// - Returns: A connection corresponding to the specified parameters
|
||||
///
|
||||
/// When the pool is asked for a new connection, it creates a `Key` from the url associated to the `request`. This key
|
||||
/// is used to determine if there already exists an associated `HTTP1ConnectionProvider` in `connectionProviders`.
|
||||
/// If there is, the connection provider then takes care of leasing a new connection. If a connection provider doesn't exist, it is created.
|
||||
func getConnection(for request: HTTPClient.Request, preference: HTTPClient.EventLoopPreference, on eventLoop: EventLoop, deadline: NIODeadline?) -> EventLoopFuture<Connection> {
|
||||
let key = Key(request)
|
||||
|
||||
let provider: HTTP1ConnectionProvider = self.connectionProvidersLock.withLock {
|
||||
if let existing = self.connectionProviders[key] {
|
||||
existing.stateLock.withLock {
|
||||
existing.state.pending += 1
|
||||
}
|
||||
return existing
|
||||
} else {
|
||||
let http1Provider = HTTP1ConnectionProvider(key: key, eventLoop: eventLoop, configuration: self.configuration, parentPool: self)
|
||||
self.connectionProviders[key] = http1Provider
|
||||
http1Provider.stateLock.withLock {
|
||||
http1Provider.state.pending += 1
|
||||
}
|
||||
return http1Provider
|
||||
}
|
||||
}
|
||||
|
||||
return provider.getConnection(preference: preference)
|
||||
}
|
||||
|
||||
func release(_ connection: Connection) {
|
||||
let connectionProvider = self.connectionProvidersLock.withLock {
|
||||
self.connectionProviders[connection.key]
|
||||
}
|
||||
if let connectionProvider = connectionProvider {
|
||||
connectionProvider.release(connection: connection)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareForClose() {
|
||||
let connectionProviders = self.connectionProvidersLock.withLock { self.connectionProviders.values }
|
||||
for connectionProvider in connectionProviders {
|
||||
connectionProvider.prepareForClose()
|
||||
}
|
||||
}
|
||||
|
||||
func syncClose() {
|
||||
let connectionProviders = self.connectionProvidersLock.withLock { self.connectionProviders.values }
|
||||
for connectionProvider in connectionProviders {
|
||||
connectionProvider.syncClose()
|
||||
}
|
||||
self.connectionProvidersLock.withLock {
|
||||
assert(self.connectionProviders.count == 0, "left-overs: \(self.connectionProviders)")
|
||||
}
|
||||
}
|
||||
|
||||
var connectionProviderCount: Int {
|
||||
return self.connectionProvidersLock.withLock {
|
||||
self.connectionProviders.count
|
||||
}
|
||||
}
|
||||
|
||||
/// Used by the `ConnectionPool` to index its `HTTP1ConnectionProvider`s
|
||||
///
|
||||
/// A key is initialized from a `URL`, it uses the components to derive a hashed value
|
||||
/// used by the `connectionProviders` dictionary to allow retrieving and creating
|
||||
/// connection providers associated to a certain request in constant time.
|
||||
struct Key: Hashable {
|
||||
init(_ request: HTTPClient.Request) {
|
||||
switch request.scheme {
|
||||
case "http":
|
||||
self.scheme = .http
|
||||
case "https":
|
||||
self.scheme = .https
|
||||
case "unix":
|
||||
self.scheme = .unix
|
||||
self.unixPath = request.url.baseURL?.path ?? request.url.path
|
||||
default:
|
||||
fatalError("HTTPClient.Request scheme should already be a valid one")
|
||||
}
|
||||
self.port = request.port
|
||||
self.host = request.host
|
||||
}
|
||||
|
||||
var scheme: Scheme
|
||||
var host: String
|
||||
var port: Int
|
||||
var unixPath: String = ""
|
||||
|
||||
enum Scheme: Hashable {
|
||||
case http
|
||||
case https
|
||||
case unix
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Connection` represents a `Channel` in the context of the connection pool
|
||||
///
|
||||
/// In the `ConnectionPool`, each `Channel` belongs to a given `HTTP1ConnectionProvider`
|
||||
/// and has a certain "lease state" (see the `isLeased` property).
|
||||
/// The role of `Connection` is to model this by storing a `Channel` alongside its associated properties
|
||||
/// so that they can be passed around together.
|
||||
///
|
||||
/// - Warning: `Connection` properties are not thread-safe and should be used with proper synchronization
|
||||
class Connection: CustomStringConvertible {
|
||||
init(key: Key, channel: Channel, parentPool: ConnectionPool) {
|
||||
self.key = key
|
||||
self.channel = channel
|
||||
self.parentPool = parentPool
|
||||
self.closePromise = channel.eventLoop.makePromise(of: Void.self)
|
||||
self.closeFuture = self.closePromise.futureResult
|
||||
}
|
||||
|
||||
/// Release this `Connection` to its associated `HTTP1ConnectionProvider` in the parent `ConnectionPool`
|
||||
///
|
||||
/// This is exactly equivalent to calling `.release(theProvider)` on `ConnectionPool`
|
||||
///
|
||||
/// - Warning: This only releases the connection and doesn't take care of cleaning handlers in the
|
||||
/// `Channel` pipeline.
|
||||
func release() {
|
||||
self.parentPool.release(self)
|
||||
}
|
||||
|
||||
func close() -> EventLoopFuture<Void> {
|
||||
self.channel.close(promise: nil)
|
||||
return self.closeFuture
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "Connection { channel: \(self.channel) }"
|
||||
}
|
||||
|
||||
/// The connection pool this `Connection` belongs to.
|
||||
///
|
||||
/// This enables calling methods like `release()` directly on a `Connection` instead of
|
||||
/// calling `pool.release(connection)`. This gives a more object oriented feel to the API
|
||||
/// and can avoid having to keep explicit references to the pool at call site.
|
||||
let parentPool: ConnectionPool
|
||||
|
||||
/// The `Key` of the `HTTP1ConnectionProvider` this `Connection` belongs to
|
||||
///
|
||||
/// This lets `ConnectionPool` know the relationship between `Connection`s and `HTTP1ConnectionProvider`s
|
||||
fileprivate let key: Key
|
||||
|
||||
/// The `Channel` of this `Connection`
|
||||
///
|
||||
/// - Warning: Requests that lease connections from the `ConnectionPool` are responsible
|
||||
/// for removing the specific handlers they added to the `Channel` pipeline before releasing it to the pool.
|
||||
let channel: Channel
|
||||
|
||||
/// Wether the connection is currently leased or not
|
||||
var isLeased: Bool = false
|
||||
|
||||
/// Indicates that this connection is about to close
|
||||
var isClosing: Bool = false
|
||||
|
||||
/// Indicates wether the usual close callback should be run or not, this allows customizing what happens
|
||||
/// on close in some cases such as for the `.replaceConnection` action
|
||||
///
|
||||
/// - Warning: This should be accessed under the `stateLock` of `HTTP1ConnectionProvider`
|
||||
fileprivate var mustRunDefaultCloseCallback: Bool = true
|
||||
|
||||
/// Convenience property indicating wether the underlying `Channel` is active or not
|
||||
var isActiveEstimation: Bool {
|
||||
return self.channel.isActive
|
||||
}
|
||||
|
||||
fileprivate var closePromise: EventLoopPromise<Void>
|
||||
|
||||
var closeFuture: EventLoopFuture<Void>
|
||||
}
|
||||
|
||||
/// A connection provider of `HTTP/1.1` connections with a given `Key` (host, scheme, port)
|
||||
///
|
||||
/// On top of enabling connection reuse this provider it also facilitates the creation
|
||||
/// of concurrent requests as it has built-in politeness regarding the maximum number
|
||||
/// of concurrent requests to the server.
|
||||
class HTTP1ConnectionProvider: CustomStringConvertible {
|
||||
/// The default `EventLoop` for this provider
|
||||
///
|
||||
/// The default event loop is used to create futures and is used
|
||||
/// when creating `Channel`s for requests for which the
|
||||
/// `EventLoopPreference` is set to `.indifferent`
|
||||
let eventLoop: EventLoop
|
||||
|
||||
/// The client configuration used to bootstrap new requests
|
||||
private let configuration: HTTPClient.Configuration
|
||||
|
||||
/// The key associated with this provider
|
||||
private let key: ConnectionPool.Key
|
||||
|
||||
/// The `State` of this provider
|
||||
///
|
||||
/// This property holds data structures representing the current state of the provider
|
||||
/// - Warning: This type isn't thread safe and should be accessed with proper
|
||||
/// synchronization (see the `stateLock` property)
|
||||
fileprivate var state: State
|
||||
|
||||
/// The lock used to access and modify the `state` property
|
||||
///
|
||||
/// - Warning: This lock should always be acquired *after* `ConnectionPool`s `connectionProvidersLock` if used in combination with it.
|
||||
fileprivate let stateLock = Lock()
|
||||
|
||||
/// The maximum number of concurrent connections to a given (host, scheme, port)
|
||||
private let maximumConcurrentConnections: Int = 8
|
||||
|
||||
/// The pool this provider belongs to
|
||||
private let parentPool: ConnectionPool
|
||||
|
||||
/// Creates a new `HTTP1ConnectionProvider`
|
||||
///
|
||||
/// - parameters:
|
||||
/// - key: The `Key` (host, scheme, port) this provider is associated to
|
||||
/// - configuration: The client configuration used globally by all requests
|
||||
/// - initialConnection: The initial connection the pool initializes this provider with
|
||||
/// - parentPool: The pool this provider belongs to
|
||||
init(key: ConnectionPool.Key, eventLoop: EventLoop, configuration: HTTPClient.Configuration, parentPool: ConnectionPool) {
|
||||
self.eventLoop = eventLoop
|
||||
self.configuration = configuration
|
||||
self.key = key
|
||||
self.parentPool = parentPool
|
||||
self.state = State(eventLoop: eventLoop, parentPool: parentPool, key: key)
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.state.activity == .closed, "Non closed on deinit")
|
||||
assert(self.state.availableConnections.isEmpty, "Available connections should be empty before deinit")
|
||||
assert(self.state.leased == 0, "All leased connections should have been returned before deinit")
|
||||
assert(self.state.waiters.count == 0, "Waiters on deinit: \(self.state.waiters)")
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "HTTP1ConnectionProvider { key: \(self.key), state: \(self.state) }"
|
||||
}
|
||||
|
||||
func getConnection(preference: HTTPClient.EventLoopPreference) -> EventLoopFuture<Connection> {
|
||||
self.activityPrecondition(expected: [.opened])
|
||||
let action = self.stateLock.withLock { self.state.connectionAction(for: preference) }
|
||||
switch action {
|
||||
case .leaseConnection(let connection):
|
||||
return connection.channel.eventLoop.makeSucceededFuture(connection)
|
||||
case .makeConnection(let eventLoop):
|
||||
return self.makeConnection(on: eventLoop)
|
||||
case .leaseFutureConnection(let futureConnection):
|
||||
return futureConnection
|
||||
}
|
||||
}
|
||||
|
||||
func release(connection: Connection) {
|
||||
self.activityPrecondition(expected: [.opened, .closing])
|
||||
let action = self.parentPool.connectionProvidersLock.withLock {
|
||||
self.stateLock.withLock { self.state.releaseAction(for: connection) }
|
||||
}
|
||||
switch action {
|
||||
case .succeed(let promise):
|
||||
promise.succeed(connection)
|
||||
|
||||
case .makeConnectionAndComplete(let eventLoop, let promise):
|
||||
self.makeConnection(on: eventLoop).cascade(to: promise)
|
||||
|
||||
case .replaceConnection(let eventLoop, let promise):
|
||||
connection.close().flatMap {
|
||||
self.makeConnection(on: eventLoop)
|
||||
}.whenComplete { result in
|
||||
switch result {
|
||||
case .success(let connection):
|
||||
promise.succeed(connection)
|
||||
case .failure(let error):
|
||||
promise.fail(error)
|
||||
}
|
||||
}
|
||||
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func makeConnection(on eventLoop: EventLoop) -> EventLoopFuture<Connection> {
|
||||
self.activityPrecondition(expected: [.opened])
|
||||
let handshakePromise = eventLoop.makePromise(of: Void.self)
|
||||
let bootstrap = ClientBootstrap.makeHTTPClientBootstrapBase(group: eventLoop, host: self.key.host, port: self.key.port, configuration: self.configuration)
|
||||
let address = HTTPClient.resolveAddress(host: self.key.host, port: self.key.port, proxy: self.configuration.proxy)
|
||||
|
||||
let channel: EventLoopFuture<Channel>
|
||||
switch self.key.scheme {
|
||||
case .http, .https:
|
||||
channel = bootstrap.connect(host: address.host, port: address.port)
|
||||
case .unix:
|
||||
channel = bootstrap.connect(unixDomainSocketPath: self.key.unixPath)
|
||||
}
|
||||
|
||||
return channel.flatMap { channel -> EventLoopFuture<ConnectionPool.Connection> in
|
||||
channel.pipeline.addSSLHandlerIfNeeded(for: self.key, tlsConfiguration: self.configuration.tlsConfiguration, handshakePromise: handshakePromise).flatMap {
|
||||
channel.pipeline.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
|
||||
}.map {
|
||||
let connection = Connection(key: self.key, channel: channel, parentPool: self.parentPool)
|
||||
connection.isLeased = true
|
||||
return connection
|
||||
}
|
||||
}.flatMap { connection in
|
||||
handshakePromise.futureResult.map {
|
||||
self.configureCloseCallback(of: connection)
|
||||
return connection
|
||||
}.flatMapError { error in
|
||||
connection.closePromise.succeed(())
|
||||
let action = self.parentPool.connectionProvidersLock.withLock {
|
||||
self.stateLock.withLock {
|
||||
self.state.failedConnectionAction()
|
||||
}
|
||||
}
|
||||
switch action {
|
||||
case .makeConnectionAndComplete(let el, let promise):
|
||||
self.makeConnection(on: el).cascade(to: promise)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
return self.eventLoop.makeFailedFuture(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a callback on connection close that asks the `state` what to do about this
|
||||
///
|
||||
/// The callback informs the state about the event, and the state returns a
|
||||
/// `ClosedConnectionRemoveAction` which instructs it about what it should do.
|
||||
private func configureCloseCallback(of connection: Connection) {
|
||||
connection.channel.closeFuture.whenComplete { result in
|
||||
let action: HTTP1ConnectionProvider.State.ClosedConnectionRemoveAction? = self.parentPool.connectionProvidersLock.withLock {
|
||||
self.stateLock.withLock {
|
||||
guard connection.mustRunDefaultCloseCallback else {
|
||||
return nil
|
||||
}
|
||||
switch result {
|
||||
case .success:
|
||||
return self.state.removeClosedConnection(connection)
|
||||
|
||||
case .failure(let error):
|
||||
preconditionFailure("Connection close future failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let action = action {
|
||||
switch action {
|
||||
case .makeConnectionAndComplete(let el, let promise):
|
||||
self.makeConnection(on: el).cascade(to: promise)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
connection.closePromise.succeed(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes and fails all `waiters`, remove existing `availableConnections` and sets `state.activity` to `.closing`
|
||||
func prepareForClose() {
|
||||
assert(MultiThreadedEventLoopGroup.currentEventLoop == nil,
|
||||
"HTTPClient shutdown on EventLoop unsupported") // calls .wait() so it would crash later anyway
|
||||
let (waitersFutures, closeFutures) = self.stateLock.withLock { () -> ([EventLoopFuture<Connection>], [EventLoopFuture<Void>]) in
|
||||
assert(self.state.activity == .opened, "Invalid activity: \(self.state.activity)")
|
||||
// Fail waiters
|
||||
let waitersCopy = self.state.waiters
|
||||
self.state.waiters.removeAll()
|
||||
let waitersPromises = waitersCopy.map { $0.promise }
|
||||
let waitersFutures = waitersPromises.map { $0.futureResult }
|
||||
waitersPromises.forEach { $0.fail(HTTPClientError.cancelled) }
|
||||
let closeFutures = self.state.availableConnections.map { $0.close() }
|
||||
return (waitersFutures, closeFutures)
|
||||
}
|
||||
try? EventLoopFuture<Connection>.andAllComplete(waitersFutures, on: self.eventLoop).wait()
|
||||
try? EventLoopFuture<Void>.andAllComplete(closeFutures, on: self.eventLoop).wait()
|
||||
|
||||
self.stateLock.withLock {
|
||||
if self.state.leased == 0, self.state.availableConnections.isEmpty {
|
||||
self.state.activity = .closed
|
||||
} else {
|
||||
self.state.activity = .closing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncClose() {
|
||||
assert(MultiThreadedEventLoopGroup.currentEventLoop == nil,
|
||||
"HTTPClient shutdown on EventLoop unsupported") // calls .wait() so it would crash later anyway
|
||||
let availableConnections = self.stateLock.withLock { () -> CircularBuffer<ConnectionPool.Connection> in
|
||||
assert(self.state.activity == .closing)
|
||||
return self.state.availableConnections
|
||||
}
|
||||
try? EventLoopFuture<Void>.andAllComplete(availableConnections.map { $0.close() }, on: self.eventLoop).wait()
|
||||
}
|
||||
|
||||
private func activityPrecondition(expected: Set<State.Activity>) {
|
||||
self.stateLock.withLock {
|
||||
precondition(expected.contains(self.state.activity), "Attempting to use HTTP1ConnectionProvider with unexpected state: \(self.state.activity) (expected: \(expected))")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct State {
|
||||
/// The default `EventLoop` to use for this `HTTP1ConnectionProvider`
|
||||
private let defaultEventLoop: EventLoop
|
||||
|
||||
/// The maximum number of connections to a certain (host, scheme, port) tuple.
|
||||
private let maximumConcurrentConnections: Int = 8
|
||||
|
||||
/// Opened connections that are available
|
||||
fileprivate var availableConnections: CircularBuffer<Connection> = .init(initialCapacity: 8)
|
||||
|
||||
/// The number of currently leased connections
|
||||
fileprivate var leased: Int = 0 {
|
||||
didSet {
|
||||
assert((0...self.maximumConcurrentConnections).contains(self.leased), "Invalid number of leased connections (\(self.leased))")
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumers that weren't able to get a new connection without exceeding
|
||||
/// `maximumConcurrentConnections` get a `Future<Connection>`
|
||||
/// whose associated promise is stored in `Waiter`. The promise is completed
|
||||
/// as soon as possible by the provider, in FIFO order.
|
||||
fileprivate var waiters: CircularBuffer<Waiter> = .init(initialCapacity: 8)
|
||||
|
||||
fileprivate var activity: Activity = .opened
|
||||
|
||||
fileprivate var pending: Int = 0
|
||||
|
||||
private let parentPool: ConnectionPool
|
||||
|
||||
private let key: Key
|
||||
|
||||
fileprivate init(eventLoop: EventLoop, parentPool: ConnectionPool, key: Key) {
|
||||
self.defaultEventLoop = eventLoop
|
||||
self.parentPool = parentPool
|
||||
self.key = key
|
||||
}
|
||||
|
||||
fileprivate mutating func connectionAction(for preference: HTTPClient.EventLoopPreference) -> ConnectionGetAction {
|
||||
self.pending -= 1
|
||||
let (channelEL, requiresSpecifiedEL) = self.resolvePreference(preference)
|
||||
if self.leased < self.maximumConcurrentConnections {
|
||||
self.leased += 1
|
||||
if let connection = availableConnections.swapWithFirstAndRemove(where: { $0.channel.eventLoop === channelEL }) {
|
||||
connection.isLeased = true
|
||||
return .leaseConnection(connection)
|
||||
} else {
|
||||
if requiresSpecifiedEL {
|
||||
return .makeConnection(channelEL)
|
||||
} else if let existingConnection = availableConnections.popFirst() {
|
||||
return .leaseConnection(existingConnection)
|
||||
} else {
|
||||
return .makeConnection(self.defaultEventLoop)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let promise = channelEL.makePromise(of: Connection.self)
|
||||
self.waiters.append(Waiter(promise: promise, preference: preference))
|
||||
return .leaseFutureConnection(promise.futureResult)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate mutating func releaseAction(for connection: Connection) -> ConnectionReleaseAction {
|
||||
if let firstWaiter = self.waiters.popFirst() {
|
||||
let (channelEL, requiresSpecifiedEL) = self.resolvePreference(firstWaiter.preference)
|
||||
|
||||
guard connection.isActiveEstimation, !connection.isClosing else {
|
||||
return .makeConnectionAndComplete(channelEL, firstWaiter.promise)
|
||||
}
|
||||
|
||||
if connection.channel.eventLoop === channelEL {
|
||||
return .succeed(firstWaiter.promise)
|
||||
} else {
|
||||
if requiresSpecifiedEL {
|
||||
connection.mustRunDefaultCloseCallback = false
|
||||
return .replaceConnection(channelEL, firstWaiter.promise)
|
||||
} else {
|
||||
return .makeConnectionAndComplete(channelEL, firstWaiter.promise)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
connection.isLeased = false
|
||||
self.leased -= 1
|
||||
if connection.isActiveEstimation, !connection.isClosing {
|
||||
self.availableConnections.append(connection)
|
||||
}
|
||||
|
||||
if self.providerMustClose() {
|
||||
self.removeFromPool()
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate mutating func removeClosedConnection(_ connection: Connection) -> ClosedConnectionRemoveAction {
|
||||
if connection.isLeased {
|
||||
if let firstWaiter = self.waiters.popFirst() {
|
||||
let (el, _) = self.resolvePreference(firstWaiter.preference)
|
||||
return .makeConnectionAndComplete(el, firstWaiter.promise)
|
||||
}
|
||||
} else {
|
||||
self.availableConnections.swapWithFirstAndRemove(where: { $0 === connection })
|
||||
}
|
||||
|
||||
if self.providerMustClose() {
|
||||
self.removeFromPool()
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
fileprivate mutating func failedConnectionAction() -> ClosedConnectionRemoveAction {
|
||||
if let firstWaiter = self.waiters.popFirst() {
|
||||
let (el, _) = self.resolvePreference(firstWaiter.preference)
|
||||
return .makeConnectionAndComplete(el, firstWaiter.promise)
|
||||
} else {
|
||||
self.leased -= 1
|
||||
if self.providerMustClose() {
|
||||
self.removeFromPool()
|
||||
}
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
private func providerMustClose() -> Bool {
|
||||
return self.pending == 0 && self.activity != .closed && self.leased == 0 && self.availableConnections.isEmpty && self.waiters.isEmpty
|
||||
}
|
||||
|
||||
/// - Warning: This should always be called from a critical section protected by `.connectionProvidersLock`
|
||||
fileprivate mutating func removeFromPool() {
|
||||
assert(self.parentPool.connectionProviders[self.key] != nil)
|
||||
self.parentPool.connectionProviders[self.key] = nil
|
||||
assert(self.activity != .closed)
|
||||
self.activity = .closed
|
||||
}
|
||||
|
||||
private func resolvePreference(_ preference: HTTPClient.EventLoopPreference) -> (EventLoop, Bool) {
|
||||
switch preference.preference {
|
||||
case .indifferent:
|
||||
return (self.defaultEventLoop, false)
|
||||
case .delegate(let el):
|
||||
return (el, false)
|
||||
case .delegateAndChannel(let el), .testOnly_exact(let el, _):
|
||||
return (el, true)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum ConnectionGetAction {
|
||||
case leaseConnection(Connection)
|
||||
case makeConnection(EventLoop)
|
||||
case leaseFutureConnection(EventLoopFuture<Connection>)
|
||||
}
|
||||
|
||||
fileprivate enum ConnectionReleaseAction {
|
||||
case succeed(EventLoopPromise<Connection>)
|
||||
case makeConnectionAndComplete(EventLoop, EventLoopPromise<Connection>)
|
||||
case replaceConnection(EventLoop, EventLoopPromise<Connection>)
|
||||
case none
|
||||
}
|
||||
|
||||
fileprivate enum ClosedConnectionRemoveAction {
|
||||
case none
|
||||
case makeConnectionAndComplete(EventLoop, EventLoopPromise<Connection>)
|
||||
}
|
||||
|
||||
/// A `Waiter` represents a request that waits for a connection when none is
|
||||
/// currently available
|
||||
///
|
||||
/// `Waiter`s are created when `maximumConcurrentConnections` is reached
|
||||
/// and we cannot create new connections anymore.
|
||||
fileprivate struct Waiter {
|
||||
/// The promise to complete once a connection is available
|
||||
let promise: EventLoopPromise<Connection>
|
||||
|
||||
/// The event loop preference associated to this particular request
|
||||
/// that the provider should respect
|
||||
let preference: HTTPClient.EventLoopPreference
|
||||
}
|
||||
|
||||
enum Activity: Hashable, CustomStringConvertible {
|
||||
case opened
|
||||
case closing
|
||||
case closed
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .opened:
|
||||
return "opened"
|
||||
case .closing:
|
||||
return "closing"
|
||||
case .closed:
|
||||
return "closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import NIOConcurrencyHelpers
|
||||
import NIOHTTP1
|
||||
import NIOHTTPCompression
|
||||
import NIOSSL
|
||||
import NIOTLS
|
||||
|
||||
/// HTTPClient class provides API for request execution.
|
||||
///
|
||||
@@ -48,7 +49,10 @@ public class HTTPClient {
|
||||
public let eventLoopGroup: EventLoopGroup
|
||||
let eventLoopGroupProvider: EventLoopGroupProvider
|
||||
let configuration: Configuration
|
||||
let isShutdown = NIOAtomic<Bool>.makeAtomic(value: false)
|
||||
let pool: ConnectionPool
|
||||
var state: State
|
||||
private var tasks = [UUID: TaskProtocol]()
|
||||
private let stateLock = Lock()
|
||||
|
||||
/// Create an `HTTPClient` with specified `EventLoopGroup` provider and configuration.
|
||||
///
|
||||
@@ -64,24 +68,79 @@ public class HTTPClient {
|
||||
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
}
|
||||
self.configuration = configuration
|
||||
self.pool = ConnectionPool(configuration: configuration)
|
||||
self.state = .upAndRunning
|
||||
}
|
||||
|
||||
deinit {
|
||||
assert(self.isShutdown.load(), "Client not shut down before the deinit. Please call client.syncShutdown() when no longer needed.")
|
||||
assert(self.pool.connectionProviderCount == 0)
|
||||
assert(self.state == .shutDown, "Client not shut down before the deinit. Please call client.syncShutdown() when no longer needed.")
|
||||
}
|
||||
|
||||
/// Shuts down the client and `EventLoopGroup` if it was created by the client.
|
||||
public func syncShutdown() throws {
|
||||
switch self.eventLoopGroupProvider {
|
||||
case .shared:
|
||||
self.isShutdown.store(true)
|
||||
return
|
||||
case .createNew:
|
||||
if self.isShutdown.compareAndExchange(expected: false, desired: true) {
|
||||
try self.eventLoopGroup.syncShutdownGracefully()
|
||||
} else {
|
||||
try self.syncShutdown(requiresCleanClose: false)
|
||||
}
|
||||
|
||||
/// Shuts down the client and `EventLoopGroup` if it was created by the client.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - requiresCleanClose: Determine if the client should throw when it is shutdown in a non-clean state
|
||||
///
|
||||
/// - Note:
|
||||
/// The `requiresCleanClose` will let the client do additional checks about its internal consistency on shutdown and
|
||||
/// throw the appropriate error if needed. For instance, if its internal connection pool has any non-released connections,
|
||||
/// this indicate shutdown was called too early before tasks were completed or explicitly canceled.
|
||||
/// In general, setting this parameter to `true` should make it easier and faster to catch related programming errors.
|
||||
internal func syncShutdown(requiresCleanClose: Bool) throws {
|
||||
var closeError: Error?
|
||||
|
||||
let tasks = try self.stateLock.withLock { () -> Dictionary<UUID, TaskProtocol>.Values in
|
||||
if self.state != .upAndRunning {
|
||||
throw HTTPClientError.alreadyShutdown
|
||||
}
|
||||
self.state = .shuttingDown
|
||||
return self.tasks.values
|
||||
}
|
||||
|
||||
self.pool.prepareForClose()
|
||||
|
||||
if !tasks.isEmpty, requiresCleanClose {
|
||||
closeError = HTTPClientError.uncleanShutdown
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
task.cancel()
|
||||
}
|
||||
|
||||
try? EventLoopFuture.andAllComplete((tasks.map { $0.completion }), on: self.eventLoopGroup.next()).wait()
|
||||
|
||||
self.pool.syncClose()
|
||||
|
||||
do {
|
||||
try self.stateLock.withLock {
|
||||
switch self.eventLoopGroupProvider {
|
||||
case .shared:
|
||||
self.state = .shutDown
|
||||
return
|
||||
case .createNew:
|
||||
switch self.state {
|
||||
case .shuttingDown:
|
||||
self.state = .shutDown
|
||||
try self.eventLoopGroup.syncShutdownGracefully()
|
||||
case .shutDown, .upAndRunning:
|
||||
assertionFailure("The only valid state at this point is \(State.shutDown)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if closeError == nil {
|
||||
closeError = error
|
||||
}
|
||||
}
|
||||
|
||||
if let closeError = closeError {
|
||||
throw closeError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,8 +247,7 @@ public class HTTPClient {
|
||||
public func execute<Delegate: HTTPClientResponseDelegate>(request: Request,
|
||||
delegate: Delegate,
|
||||
deadline: NIODeadline? = nil) -> Task<Delegate.Response> {
|
||||
let eventLoop = self.eventLoopGroup.next()
|
||||
return self.execute(request: request, delegate: delegate, eventLoop: eventLoop, deadline: deadline)
|
||||
return self.execute(request: request, delegate: delegate, eventLoop: .indifferent, deadline: deadline)
|
||||
}
|
||||
|
||||
/// Execute arbitrary HTTP request and handle response processing using provided delegate.
|
||||
@@ -201,31 +259,35 @@ public class HTTPClient {
|
||||
/// - deadline: Point in time by which the request must complete.
|
||||
public func execute<Delegate: HTTPClientResponseDelegate>(request: Request,
|
||||
delegate: Delegate,
|
||||
eventLoop: EventLoopPreference,
|
||||
eventLoop eventLoopPreference: EventLoopPreference,
|
||||
deadline: NIODeadline? = nil) -> Task<Delegate.Response> {
|
||||
switch eventLoop.preference {
|
||||
let taskEL: EventLoop
|
||||
switch eventLoopPreference.preference {
|
||||
case .indifferent:
|
||||
return self.execute(request: request, delegate: delegate, eventLoop: self.eventLoopGroup.next(), deadline: deadline)
|
||||
taskEL = self.pool.associatedEventLoop(for: ConnectionPool.Key(request)) ?? self.eventLoopGroup.next()
|
||||
case .delegate(on: let eventLoop):
|
||||
precondition(self.eventLoopGroup.makeIterator().contains { $0 === eventLoop }, "Provided EventLoop must be part of clients EventLoopGroup.")
|
||||
return self.execute(request: request, delegate: delegate, eventLoop: eventLoop, deadline: deadline)
|
||||
taskEL = eventLoop
|
||||
case .delegateAndChannel(on: let eventLoop):
|
||||
precondition(self.eventLoopGroup.makeIterator().contains { $0 === eventLoop }, "Provided EventLoop must be part of clients EventLoopGroup.")
|
||||
return self.execute(request: request, delegate: delegate, eventLoop: eventLoop, deadline: deadline)
|
||||
case .testOnly_exact(channelOn: let channelEL, delegateOn: let delegateEL):
|
||||
return self.execute(request: request,
|
||||
delegate: delegate,
|
||||
eventLoop: delegateEL,
|
||||
channelEL: channelEL,
|
||||
deadline: deadline)
|
||||
taskEL = eventLoop
|
||||
case .testOnly_exact(_, delegateOn: let delegateEL):
|
||||
taskEL = delegateEL
|
||||
}
|
||||
|
||||
let failedTask: Task<Delegate.Response>? = self.stateLock.withLock {
|
||||
switch state {
|
||||
case .upAndRunning:
|
||||
return nil
|
||||
case .shuttingDown, .shutDown:
|
||||
return Task<Delegate.Response>.failedTask(eventLoop: taskEL, error: HTTPClientError.alreadyShutdown)
|
||||
}
|
||||
}
|
||||
|
||||
if let failedTask = failedTask {
|
||||
return failedTask
|
||||
}
|
||||
}
|
||||
|
||||
private func execute<Delegate: HTTPClientResponseDelegate>(request: Request,
|
||||
delegate: Delegate,
|
||||
eventLoop delegateEL: EventLoop,
|
||||
channelEL: EventLoop? = nil,
|
||||
deadline: NIODeadline? = nil) -> Task<Delegate.Response> {
|
||||
let redirectHandler: RedirectHandler<Delegate.Response>?
|
||||
switch self.configuration.redirectConfiguration.configuration {
|
||||
case .follow(let max, let allowCycles):
|
||||
@@ -236,72 +298,73 @@ public class HTTPClient {
|
||||
redirectHandler = RedirectHandler<Delegate.Response>(request: request) { newRequest in
|
||||
self.execute(request: newRequest,
|
||||
delegate: delegate,
|
||||
eventLoop: delegateEL,
|
||||
channelEL: channelEL,
|
||||
eventLoop: eventLoopPreference,
|
||||
deadline: deadline)
|
||||
}
|
||||
case .disallow:
|
||||
redirectHandler = nil
|
||||
}
|
||||
|
||||
let task = Task<Delegate.Response>(eventLoop: delegateEL)
|
||||
let task = Task<Delegate.Response>(eventLoop: taskEL)
|
||||
self.stateLock.withLock {
|
||||
self.tasks[task.id] = task
|
||||
}
|
||||
let promise = task.promise
|
||||
|
||||
var bootstrap = ClientBootstrap(group: channelEL ?? delegateEL)
|
||||
.channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1)
|
||||
.channelInitializer { channel in
|
||||
let encoder = HTTPRequestEncoder()
|
||||
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes))
|
||||
return channel.pipeline.addHandlers([encoder, decoder], position: .first).flatMap {
|
||||
switch self.configuration.proxy {
|
||||
case .none:
|
||||
return channel.pipeline.addSSLHandlerIfNeeded(for: request, tlsConfiguration: self.configuration.tlsConfiguration)
|
||||
case .some(let proxy):
|
||||
return channel.pipeline.addProxyHandler(for: request, decoder: decoder, encoder: encoder, tlsConfiguration: self.configuration.tlsConfiguration, proxy: proxy)
|
||||
}
|
||||
}.flatMap {
|
||||
switch self.configuration.decompression {
|
||||
case .disabled:
|
||||
return channel.eventLoop.makeSucceededFuture(())
|
||||
case .enabled(let limit):
|
||||
return channel.pipeline.addHandler(NIOHTTPResponseDecompressor(limit: limit))
|
||||
}
|
||||
}.flatMap {
|
||||
if let timeout = self.resolve(timeout: self.configuration.timeout.read, deadline: deadline) {
|
||||
return channel.pipeline.addHandler(IdleStateHandler(readTimeout: timeout))
|
||||
} else {
|
||||
return channel.eventLoop.makeSucceededFuture(())
|
||||
}
|
||||
}.flatMap {
|
||||
let taskHandler = TaskHandler(task: task,
|
||||
kind: request.kind,
|
||||
delegate: delegate,
|
||||
redirectHandler: redirectHandler,
|
||||
ignoreUncleanSSLShutdown: self.configuration.ignoreUncleanSSLShutdown)
|
||||
return channel.pipeline.addHandler(taskHandler)
|
||||
}
|
||||
promise.futureResult.whenComplete { _ in
|
||||
self.stateLock.withLock {
|
||||
self.tasks[task.id] = nil
|
||||
}
|
||||
}
|
||||
|
||||
let connection = self.pool.getConnection(for: request, preference: eventLoopPreference, on: taskEL, deadline: deadline)
|
||||
|
||||
connection.flatMap { connection -> EventLoopFuture<Void> in
|
||||
let channel = connection.channel
|
||||
let addedFuture: EventLoopFuture<Void>
|
||||
|
||||
switch self.configuration.decompression {
|
||||
case .disabled:
|
||||
addedFuture = channel.eventLoop.makeSucceededFuture(())
|
||||
case .enabled(let limit):
|
||||
let decompressHandler = NIOHTTPResponseDecompressor(limit: limit)
|
||||
addedFuture = channel.pipeline.addHandler(decompressHandler)
|
||||
}
|
||||
|
||||
if let timeout = self.resolve(timeout: self.configuration.timeout.connect, deadline: deadline) {
|
||||
bootstrap = bootstrap.connectTimeout(timeout)
|
||||
}
|
||||
return addedFuture.flatMap {
|
||||
if let timeout = self.resolve(timeout: self.configuration.timeout.read, deadline: deadline) {
|
||||
return channel.pipeline.addHandler(IdleStateHandler(readTimeout: timeout))
|
||||
} else {
|
||||
return channel.eventLoop.makeSucceededFuture(())
|
||||
}
|
||||
}.flatMap {
|
||||
let taskHandler = TaskHandler(task: task,
|
||||
kind: request.kind,
|
||||
delegate: delegate,
|
||||
redirectHandler: redirectHandler,
|
||||
ignoreUncleanSSLShutdown: self.configuration.ignoreUncleanSSLShutdown)
|
||||
return channel.pipeline.addHandler(taskHandler)
|
||||
}.flatMap {
|
||||
task.setConnection(connection)
|
||||
|
||||
let eventLoopChannel: EventLoopFuture<Channel>
|
||||
switch request.kind {
|
||||
case .unixSocket:
|
||||
let socketPath = request.url.baseURL?.path ?? request.url.path
|
||||
eventLoopChannel = bootstrap.connect(unixDomainSocketPath: socketPath)
|
||||
case .host:
|
||||
let address = self.resolveAddress(request: request, proxy: self.configuration.proxy)
|
||||
eventLoopChannel = bootstrap.connect(host: address.host, port: address.port)
|
||||
}
|
||||
let isCancelled = task.lock.withLock {
|
||||
task.cancelled
|
||||
}
|
||||
|
||||
eventLoopChannel.map { channel in
|
||||
task.setChannel(channel)
|
||||
}
|
||||
.flatMap { channel in
|
||||
channel.writeAndFlush(request)
|
||||
}
|
||||
.cascadeFailure(to: task.promise)
|
||||
if !isCancelled {
|
||||
return channel.writeAndFlush(request).flatMapError { _ in
|
||||
// At this point the `TaskHandler` will already be present
|
||||
// to handle the failure and pass it to the `promise`
|
||||
channel.eventLoop.makeSucceededFuture(())
|
||||
}
|
||||
} else {
|
||||
return channel.eventLoop.makeSucceededFuture(())
|
||||
}
|
||||
}.flatMapError { error in
|
||||
connection.release()
|
||||
return channel.eventLoop.makeFailedFuture(error)
|
||||
}
|
||||
}.cascadeFailure(to: promise)
|
||||
|
||||
return task
|
||||
}
|
||||
@@ -319,10 +382,10 @@ public class HTTPClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveAddress(request: Request, proxy: Configuration.Proxy?) -> (host: String, port: Int) {
|
||||
switch self.configuration.proxy {
|
||||
static func resolveAddress(host: String, port: Int, proxy: Configuration.Proxy?) -> (host: String, port: Int) {
|
||||
switch proxy {
|
||||
case .none:
|
||||
return (request.host, request.port)
|
||||
return (host, port)
|
||||
case .some(let proxy):
|
||||
return (proxy.host, proxy.port)
|
||||
}
|
||||
@@ -436,6 +499,12 @@ public class HTTPClient {
|
||||
/// Decompression is enabled.
|
||||
case enabled(limit: NIOHTTPDecompression.DecompressionLimit)
|
||||
}
|
||||
|
||||
enum State {
|
||||
case upAndRunning
|
||||
case shuttingDown
|
||||
case shutDown
|
||||
}
|
||||
}
|
||||
|
||||
extension HTTPClient.Configuration {
|
||||
@@ -490,37 +559,84 @@ extension HTTPClient.Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
private extension ChannelPipeline {
|
||||
func addProxyHandler(for request: HTTPClient.Request, decoder: ByteToMessageHandler<HTTPResponseDecoder>, encoder: HTTPRequestEncoder, tlsConfiguration: TLSConfiguration?, proxy: HTTPClient.Configuration.Proxy?) -> EventLoopFuture<Void> {
|
||||
let handler = HTTPClientProxyHandler(host: request.host, port: request.port, authorization: proxy?.authorization, onConnect: { channel in
|
||||
channel.pipeline.removeHandler(decoder).flatMap {
|
||||
channel.pipeline.addHandler(
|
||||
ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes)),
|
||||
position: .after(encoder)
|
||||
)
|
||||
}.flatMap {
|
||||
channel.pipeline.addSSLHandlerIfNeeded(for: request, tlsConfiguration: tlsConfiguration)
|
||||
extension ChannelPipeline {
|
||||
func addProxyHandler(host: String, port: Int, authorization: HTTPClient.Authorization?) -> EventLoopFuture<Void> {
|
||||
let encoder = HTTPRequestEncoder()
|
||||
let decoder = ByteToMessageHandler(HTTPResponseDecoder(leftOverBytesStrategy: .forwardBytes))
|
||||
let handler = HTTPClientProxyHandler(host: host, port: port, authorization: authorization) { channel in
|
||||
let encoderRemovePromise = self.eventLoop.next().makePromise(of: Void.self)
|
||||
channel.pipeline.removeHandler(encoder, promise: encoderRemovePromise)
|
||||
return encoderRemovePromise.futureResult.flatMap {
|
||||
channel.pipeline.removeHandler(decoder)
|
||||
}
|
||||
})
|
||||
return self.addHandler(handler)
|
||||
}
|
||||
return addHandlers([encoder, decoder, handler])
|
||||
}
|
||||
|
||||
func addSSLHandlerIfNeeded(for request: HTTPClient.Request, tlsConfiguration: TLSConfiguration?) -> EventLoopFuture<Void> {
|
||||
guard request.useTLS else {
|
||||
func addSSLHandlerIfNeeded(for key: ConnectionPool.Key, tlsConfiguration: TLSConfiguration?, handshakePromise: EventLoopPromise<Void>) -> EventLoopFuture<Void> {
|
||||
guard key.scheme == .https else {
|
||||
handshakePromise.succeed(())
|
||||
return self.eventLoop.makeSucceededFuture(())
|
||||
}
|
||||
|
||||
do {
|
||||
let tlsConfiguration = tlsConfiguration ?? TLSConfiguration.forClient()
|
||||
let context = try NIOSSLContext(configuration: tlsConfiguration)
|
||||
return self.addHandler(try NIOSSLClientHandler(context: context, serverHostname: request.host.isIPAddress ? nil : request.host),
|
||||
position: .first)
|
||||
let handlers: [ChannelHandler] = [
|
||||
try NIOSSLClientHandler(context: context, serverHostname: key.host.isIPAddress ? nil : key.host),
|
||||
TLSEventsHandler(completionPromise: handshakePromise),
|
||||
]
|
||||
|
||||
return self.addHandlers(handlers)
|
||||
} catch {
|
||||
return self.eventLoop.makeFailedFuture(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TLSEventsHandler: ChannelInboundHandler, RemovableChannelHandler {
|
||||
typealias InboundIn = NIOAny
|
||||
|
||||
var completionPromise: EventLoopPromise<Void>?
|
||||
|
||||
init(completionPromise: EventLoopPromise<Void>) {
|
||||
self.completionPromise = completionPromise
|
||||
}
|
||||
|
||||
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
|
||||
if let tlsEvent = event as? TLSUserEvent {
|
||||
switch tlsEvent {
|
||||
case .handshakeCompleted:
|
||||
self.completionPromise?.succeed(())
|
||||
self.completionPromise = nil
|
||||
context.pipeline.removeHandler(self, promise: nil)
|
||||
case .shutdownCompleted:
|
||||
break
|
||||
}
|
||||
}
|
||||
context.fireUserInboundEventTriggered(event)
|
||||
}
|
||||
|
||||
func errorCaught(context: ChannelHandlerContext, error: Error) {
|
||||
if let sslError = error as? NIOSSLError {
|
||||
switch sslError {
|
||||
case .handshakeFailed:
|
||||
self.completionPromise?.fail(error)
|
||||
self.completionPromise = nil
|
||||
context.pipeline.removeHandler(self, promise: nil)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
context.fireErrorCaught(error)
|
||||
}
|
||||
|
||||
func handlerRemoved(context: ChannelHandlerContext) {
|
||||
struct NoResult: Error {}
|
||||
self.completionPromise?.fail(NoResult())
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible client errors.
|
||||
public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
|
||||
private enum Code: Equatable {
|
||||
@@ -539,6 +655,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
|
||||
case proxyAuthenticationRequired
|
||||
case redirectLimitReached
|
||||
case redirectCycleDetected
|
||||
case uncleanShutdown
|
||||
}
|
||||
|
||||
private var code: Code
|
||||
@@ -581,4 +698,6 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
|
||||
public static let redirectLimitReached = HTTPClientError(code: .redirectLimitReached)
|
||||
/// Redirect Cycle detected.
|
||||
public static let redirectCycleDetected = HTTPClientError(code: .redirectCycleDetected)
|
||||
/// Unclean shutdown
|
||||
public static let uncleanShutdown = HTTPClientError(code: .uncleanShutdown)
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan
|
||||
case awaitingResponse
|
||||
case connecting
|
||||
case connected
|
||||
case failed
|
||||
}
|
||||
|
||||
private let host: String
|
||||
@@ -102,6 +103,7 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan
|
||||
// blank line that concludes the successful response's header section
|
||||
break
|
||||
case 407:
|
||||
self.readState = .failed
|
||||
context.fireErrorCaught(HTTPClientError.proxyAuthenticationRequired)
|
||||
default:
|
||||
// Any response other than a successful response
|
||||
@@ -119,6 +121,8 @@ internal final class HTTPClientProxyHandler: ChannelDuplexHandler, RemovableChan
|
||||
self.readBuffer.append(data)
|
||||
case .connected:
|
||||
context.fireChannelRead(data)
|
||||
case .failed:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import NIO
|
||||
import NIOConcurrencyHelpers
|
||||
import NIOFoundationCompat
|
||||
import NIOHTTP1
|
||||
import NIOHTTPCompression
|
||||
import NIOSSL
|
||||
|
||||
extension HTTPClient {
|
||||
@@ -486,22 +487,31 @@ extension URL {
|
||||
extension HTTPClient {
|
||||
/// Response execution context. Will be created by the library and could be used for obtaining
|
||||
/// `EventLoopFuture<Response>` of the execution or cancellation of the execution.
|
||||
public final class Task<Response> {
|
||||
public final class Task<Response>: TaskProtocol {
|
||||
/// The `EventLoop` the delegate will be executed on.
|
||||
public let eventLoop: EventLoop
|
||||
|
||||
let promise: EventLoopPromise<Response>
|
||||
var channel: Channel?
|
||||
private var cancelled: Bool
|
||||
private let lock: Lock
|
||||
var completion: EventLoopFuture<Void>
|
||||
var connection: ConnectionPool.Connection?
|
||||
var cancelled: Bool
|
||||
let lock: Lock
|
||||
let id = UUID()
|
||||
|
||||
init(eventLoop: EventLoop) {
|
||||
self.eventLoop = eventLoop
|
||||
self.promise = eventLoop.makePromise()
|
||||
self.completion = self.promise.futureResult.map { _ in }
|
||||
self.cancelled = false
|
||||
self.lock = Lock()
|
||||
}
|
||||
|
||||
static func failedTask(eventLoop: EventLoop, error: Error) -> Task<Response> {
|
||||
let task = self.init(eventLoop: eventLoop)
|
||||
task.promise.fail(error)
|
||||
return task
|
||||
}
|
||||
|
||||
/// `EventLoopFuture` for the response returned by this request.
|
||||
public var futureResult: EventLoopFuture<Response> {
|
||||
return self.promise.futureResult
|
||||
@@ -520,18 +530,58 @@ extension HTTPClient {
|
||||
let channel: Channel? = self.lock.withLock {
|
||||
if !cancelled {
|
||||
cancelled = true
|
||||
return self.channel
|
||||
return self.connection?.channel
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
channel?.triggerUserOutboundEvent(TaskCancelEvent(), promise: nil)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func setChannel(_ channel: Channel) -> Channel {
|
||||
func setConnection(_ connection: ConnectionPool.Connection) -> ConnectionPool.Connection {
|
||||
return self.lock.withLock {
|
||||
self.channel = channel
|
||||
return channel
|
||||
self.connection = connection
|
||||
if self.cancelled {
|
||||
connection.channel.triggerUserOutboundEvent(TaskCancelEvent(), promise: nil)
|
||||
}
|
||||
return connection
|
||||
}
|
||||
}
|
||||
|
||||
func succeed<Delegate: HTTPClientResponseDelegate>(promise: EventLoopPromise<Response>?, with value: Response, delegateType: Delegate.Type) {
|
||||
self.releaseAssociatedConnection(delegateType: delegateType).whenSuccess {
|
||||
promise?.succeed(value)
|
||||
}
|
||||
}
|
||||
|
||||
func fail<Delegate: HTTPClientResponseDelegate>(with error: Error, delegateType: Delegate.Type) {
|
||||
if let connection = self.connection {
|
||||
connection.close().whenComplete { _ in
|
||||
self.releaseAssociatedConnection(delegateType: delegateType).whenComplete { _ in
|
||||
self.promise.fail(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func releaseAssociatedConnection<Delegate: HTTPClientResponseDelegate>(delegateType: Delegate.Type) -> EventLoopFuture<Void> {
|
||||
if let connection = self.connection {
|
||||
return connection.removeHandler(NIOHTTPResponseDecompressor.self).flatMap {
|
||||
connection.removeHandler(IdleStateHandler.self)
|
||||
}.flatMap {
|
||||
connection.removeHandler(TaskHandler<Delegate>.self)
|
||||
}.map {
|
||||
connection.release()
|
||||
}.flatMapError { error in
|
||||
fatalError("Couldn't remove taskHandler: \(error)")
|
||||
}
|
||||
|
||||
} else {
|
||||
// TODO: This seems only reached in some internal unit test
|
||||
// Maybe there could be a better handling in the future to make
|
||||
// it an error outside of testing contexts
|
||||
return self.eventLoop.makeSucceededFuture(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,9 +589,15 @@ extension HTTPClient {
|
||||
|
||||
internal struct TaskCancelEvent {}
|
||||
|
||||
internal protocol TaskProtocol {
|
||||
func cancel()
|
||||
var id: UUID { get }
|
||||
var completion: EventLoopFuture<Void> { get }
|
||||
}
|
||||
|
||||
// MARK: - TaskHandler
|
||||
|
||||
internal class TaskHandler<Delegate: HTTPClientResponseDelegate> {
|
||||
internal class TaskHandler<Delegate: HTTPClientResponseDelegate>: RemovableChannelHandler {
|
||||
enum State {
|
||||
case idle
|
||||
case sent
|
||||
@@ -581,7 +637,7 @@ extension TaskHandler {
|
||||
_ body: @escaping (HTTPClient.Task<Delegate.Response>, Err) -> Void) {
|
||||
func doIt() {
|
||||
body(self.task, error)
|
||||
self.task.promise.fail(error)
|
||||
self.task.fail(with: error, delegateType: Delegate.self)
|
||||
}
|
||||
|
||||
if self.task.eventLoop.inEventLoop {
|
||||
@@ -621,13 +677,14 @@ extension TaskHandler {
|
||||
}
|
||||
|
||||
func callOutToDelegate<Response>(promise: EventLoopPromise<Response>? = nil,
|
||||
_ body: @escaping (HTTPClient.Task<Delegate.Response>) throws -> Response) {
|
||||
_ body: @escaping (HTTPClient.Task<Delegate.Response>) throws -> Response) where Response == Delegate.Response {
|
||||
func doIt() {
|
||||
do {
|
||||
let result = try body(self.task)
|
||||
promise?.succeed(result)
|
||||
|
||||
self.task.succeed(promise: promise, with: result, delegateType: Delegate.self)
|
||||
} catch {
|
||||
promise?.fail(error)
|
||||
self.task.fail(with: error, delegateType: Delegate.self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,7 +698,7 @@ extension TaskHandler {
|
||||
}
|
||||
|
||||
func callOutToDelegate<Response>(channelEventLoop: EventLoop,
|
||||
_ body: @escaping (HTTPClient.Task<Delegate.Response>) throws -> Response) -> EventLoopFuture<Response> {
|
||||
_ body: @escaping (HTTPClient.Task<Delegate.Response>) throws -> Response) -> EventLoopFuture<Response> where Response == Delegate.Response {
|
||||
let promise = channelEventLoop.makePromise(of: Response.self)
|
||||
self.callOutToDelegate(promise: promise, body)
|
||||
return promise.futureResult
|
||||
@@ -678,8 +735,6 @@ extension TaskHandler: ChannelDuplexHandler {
|
||||
headers.add(name: "Host", value: request.host)
|
||||
}
|
||||
|
||||
headers.add(name: "Connection", value: "close")
|
||||
|
||||
do {
|
||||
try headers.validate(body: request.body)
|
||||
} catch {
|
||||
@@ -702,16 +757,10 @@ extension TaskHandler: ChannelDuplexHandler {
|
||||
context.eventLoop.assertInEventLoop()
|
||||
self.state = .sent
|
||||
self.callOutToDelegateFireAndForget(self.delegate.didSendRequest)
|
||||
|
||||
let channel = context.channel
|
||||
self.task.futureResult.whenComplete { _ in
|
||||
channel.close(promise: nil)
|
||||
}
|
||||
}.flatMapErrorThrowing { error in
|
||||
context.eventLoop.assertInEventLoop()
|
||||
self.state = .end
|
||||
self.failTaskAndNotifyDelegate(error: error, self.delegate.didReceiveError)
|
||||
context.close(promise: nil)
|
||||
throw error
|
||||
}.cascade(to: promise)
|
||||
}
|
||||
@@ -742,6 +791,16 @@ extension TaskHandler: ChannelDuplexHandler {
|
||||
let response = self.unwrapInboundIn(data)
|
||||
switch response {
|
||||
case .head(let head):
|
||||
if !head.isKeepAlive {
|
||||
self.task.lock.withLock {
|
||||
if let connection = self.task.connection {
|
||||
connection.isClosing = true
|
||||
} else {
|
||||
preconditionFailure("There should always be a connection at this point")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let redirectURL = redirectHandler?.redirectTarget(status: head.status, headers: head.headers) {
|
||||
self.state = .redirected(head, redirectURL)
|
||||
} else {
|
||||
@@ -768,8 +827,9 @@ extension TaskHandler: ChannelDuplexHandler {
|
||||
switch self.state {
|
||||
case .redirected(let head, let redirectURL):
|
||||
self.state = .end
|
||||
self.redirectHandler?.redirect(status: head.status, to: redirectURL, promise: self.task.promise)
|
||||
context.close(promise: nil)
|
||||
self.task.releaseAssociatedConnection(delegateType: Delegate.self).whenSuccess {
|
||||
self.redirectHandler?.redirect(status: head.status, to: redirectURL, promise: self.task.promise)
|
||||
}
|
||||
default:
|
||||
self.state = .end
|
||||
self.callOutToDelegate(promise: self.task.promise, self.delegate.didFinishRequest)
|
||||
@@ -845,6 +905,13 @@ extension TaskHandler: ChannelDuplexHandler {
|
||||
self.failTaskAndNotifyDelegate(error: error, self.delegate.didReceiveError)
|
||||
}
|
||||
}
|
||||
|
||||
func handlerAdded(context: ChannelHandlerContext) {
|
||||
guard context.channel.isActive else {
|
||||
self.failTaskAndNotifyDelegate(error: HTTPClientError.remoteConnectionClosed, self.delegate.didReceiveError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RedirectHandler
|
||||
@@ -931,9 +998,13 @@ internal struct RedirectHandler<ResponseType> {
|
||||
do {
|
||||
var newRequest = try HTTPClient.Request(url: redirectURL, method: method, headers: headers, body: body)
|
||||
newRequest.redirectState = nextState
|
||||
return self.execute(newRequest).futureResult.cascade(to: promise)
|
||||
self.execute(newRequest).futureResult.whenComplete { result in
|
||||
promise.futureResult.eventLoop.execute {
|
||||
promise.completeWith(result)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return promise.fail(error)
|
||||
promise.fail(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import NIO
|
||||
import NIOHTTP1
|
||||
import NIOHTTPCompression
|
||||
|
||||
internal extension String {
|
||||
var isIPAddress: Bool {
|
||||
@@ -44,3 +45,53 @@ public final class HTTPClientCopyingDelegate: HTTPClientResponseDelegate {
|
||||
return ()
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientBootstrap {
|
||||
static func makeHTTPClientBootstrapBase(group: EventLoopGroup, host: String, port: Int, configuration: HTTPClient.Configuration, channelInitializer: ((Channel) -> EventLoopFuture<Void>)? = nil) -> ClientBootstrap {
|
||||
return ClientBootstrap(group: group)
|
||||
.channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1)
|
||||
|
||||
.channelInitializer { channel in
|
||||
let channelAddedFuture: EventLoopFuture<Void>
|
||||
switch configuration.proxy {
|
||||
case .none:
|
||||
channelAddedFuture = group.next().makeSucceededFuture(())
|
||||
case .some:
|
||||
channelAddedFuture = channel.pipeline.addProxyHandler(host: host, port: port, authorization: configuration.proxy?.authorization)
|
||||
}
|
||||
return channelAddedFuture.flatMap { (_: Void) -> EventLoopFuture<Void> in
|
||||
channelInitializer?(channel) ?? group.next().makeSucceededFuture(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CircularBuffer {
|
||||
@discardableResult
|
||||
mutating func swapWithFirstAndRemove(at index: Index) -> Element? {
|
||||
precondition(index >= self.startIndex && index < self.endIndex)
|
||||
if !self.isEmpty {
|
||||
self.swapAt(self.startIndex, index)
|
||||
return self.removeFirst()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
mutating func swapWithFirstAndRemove(where predicate: (Element) throws -> Bool) rethrows -> Element? {
|
||||
if let existingIndex = try self.firstIndex(where: predicate) {
|
||||
return self.swapWithFirstAndRemove(at: existingIndex)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConnectionPool.Connection {
|
||||
func removeHandler<Handler: RemovableChannelHandler>(_ type: Handler.Type) -> EventLoopFuture<Void> {
|
||||
return self.channel.pipeline.handler(type: type).flatMap { handler in
|
||||
self.channel.pipeline.removeHandler(handler)
|
||||
}.recover { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user