mirror of
https://github.com/swift-server/async-http-client.git
synced 2026-05-03 07:32:29 +00:00
c65b2ae297
* Introduce helper methods for test of ConnectionsState Motivation: Issue #234 highlights that we directly manipulate ConnectionsState and this commit prepares tests to be refactored to manipulate the state by exposed APIs instead. Modifications: * introduce helper methods in ConnectionPoolTestsSupport.swift Result: * no observable changes * Move Connection tests out of ConnectionsState tests into separate file. Motivation: Clean up of code to address issue #234 - here we move away connection tests to separate files outside of ConnectionsState tests so we will be able to work on the ConnectionsState in focussed mode. Modifications: Connection tests moved to separate files. Result: No observable changes. * Gather Connection code into Connection.swift Motivation: For tests we will need a simple version of Connection, so here I gather Connection code in one place and will generify ConnectionsState on next commit. Modifications: Code of Connection is moved from multiple files into single Connections.swift. Result: All tests are passing, no observable behaviour change. * Introduce generic type ConnectionType into ConnectionsState Motivation: To rework tests of ConnectionsState we want to have a "simpler" version of Connection to be used, therefore here we convert ConnectionsState to support generic type ConnectionType. We will substitute current Connection with a test version in follow up commit. Modifications: ConnectionsState is altered to work on generic type ConnectionType instead of solid type Connection. Users of ConnectionsState are modified to provide type Connection into ConnectionType in this commit. Result: Test are passing, no observable behaviour change.
175 lines
6.1 KiB
Swift
175 lines
6.1 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// 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 Logging
|
|
import NIO
|
|
import NIOConcurrencyHelpers
|
|
import NIOHTTP1
|
|
import NIOHTTPCompression
|
|
import NIOTLS
|
|
import NIOTransportServices
|
|
|
|
/// 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 `inUse` 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 and correct provider can be identified when connection is released.
|
|
class Connection {
|
|
/// The provider this `Connection` belongs to.
|
|
///
|
|
/// This enables calling methods like `release()` directly on a `Connection` instead of
|
|
/// calling `provider.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.
|
|
private let provider: HTTP1ConnectionProvider
|
|
|
|
/// 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
|
|
|
|
init(channel: Channel, provider: HTTP1ConnectionProvider) {
|
|
self.channel = channel
|
|
self.provider = provider
|
|
}
|
|
}
|
|
|
|
extension Connection {
|
|
/// Release this `Connection` to its associated `HTTP1ConnectionProvider`.
|
|
///
|
|
/// - Warning: This only releases the connection and doesn't take care of cleaning handlers in the `Channel` pipeline.
|
|
func release(closing: Bool, logger: Logger) {
|
|
self.channel.eventLoop.assertInEventLoop()
|
|
self.provider.release(connection: self, closing: closing, logger: logger)
|
|
}
|
|
|
|
/// Called when channel exceeds idle time in pool.
|
|
func timeout(logger: Logger) {
|
|
self.channel.eventLoop.assertInEventLoop()
|
|
self.provider.timeout(connection: self, logger: logger)
|
|
}
|
|
|
|
/// Called when channel goes inactive while in the pool.
|
|
func remoteClosed(logger: Logger) {
|
|
self.channel.eventLoop.assertInEventLoop()
|
|
self.provider.remoteClosed(connection: self, logger: logger)
|
|
}
|
|
|
|
/// Called from `HTTP1ConnectionProvider.close` when client is shutting down.
|
|
func close() -> EventLoopFuture<Void> {
|
|
return self.channel.close()
|
|
}
|
|
}
|
|
|
|
/// Methods of Connection which are used in ConnectionsState extracted as protocol
|
|
/// to facilitate test of ConnectionsState.
|
|
protocol PoolManageableConnection: AnyObject {
|
|
func cancel() -> EventLoopFuture<Void>
|
|
var eventLoop: EventLoop { get }
|
|
var isActiveEstimation: Bool { get }
|
|
}
|
|
|
|
/// Implementation of methods used by ConnectionsState and its tests to manage Connection
|
|
extension Connection: PoolManageableConnection {
|
|
/// Convenience property indicating whether the underlying `Channel` is active or not.
|
|
var isActiveEstimation: Bool {
|
|
return self.channel.isActive
|
|
}
|
|
|
|
var eventLoop: EventLoop {
|
|
return self.channel.eventLoop
|
|
}
|
|
|
|
func cancel() -> EventLoopFuture<Void> {
|
|
return self.channel.triggerUserOutboundEvent(TaskCancelEvent())
|
|
}
|
|
}
|
|
|
|
extension Connection {
|
|
/// Sets idle timeout handler and channel inactivity listener.
|
|
func setIdleTimeout(timeout: TimeAmount?, logger: Logger) {
|
|
_ = self.channel.pipeline.addHandler(IdleStateHandler(writeTimeout: timeout), position: .first).flatMap { _ in
|
|
self.channel.pipeline.addHandler(IdlePoolConnectionHandler(connection: self, logger: logger))
|
|
}
|
|
}
|
|
|
|
/// Removes idle timeout handler and channel inactivity listener
|
|
func cancelIdleTimeout() -> EventLoopFuture<Void> {
|
|
return self.removeHandler(IdleStateHandler.self).flatMap { _ in
|
|
self.removeHandler(IdlePoolConnectionHandler.self)
|
|
}
|
|
}
|
|
}
|
|
|
|
class IdlePoolConnectionHandler: ChannelInboundHandler, RemovableChannelHandler {
|
|
typealias InboundIn = NIOAny
|
|
|
|
let connection: Connection
|
|
var eventSent: Bool
|
|
let logger: Logger
|
|
|
|
init(connection: Connection, logger: Logger) {
|
|
self.connection = connection
|
|
self.eventSent = false
|
|
self.logger = logger
|
|
}
|
|
|
|
// this is needed to detect when remote end closes connection while connection is in the pool idling
|
|
func channelInactive(context: ChannelHandlerContext) {
|
|
if !self.eventSent {
|
|
self.eventSent = true
|
|
self.connection.remoteClosed(logger: self.logger)
|
|
}
|
|
}
|
|
|
|
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
|
|
if let idleEvent = event as? IdleStateHandler.IdleStateEvent, idleEvent == .write {
|
|
if !self.eventSent {
|
|
self.eventSent = true
|
|
self.connection.timeout(logger: self.logger)
|
|
}
|
|
} else {
|
|
context.fireUserInboundEventTriggered(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Connection: CustomStringConvertible {
|
|
var description: String {
|
|
return "\(self.channel)"
|
|
}
|
|
}
|
|
|
|
struct ConnectionKey<ConnectionType>: Hashable where ConnectionType: PoolManageableConnection {
|
|
let connection: ConnectionType
|
|
|
|
init(_ connection: ConnectionType) {
|
|
self.connection = connection
|
|
}
|
|
|
|
static func == (lhs: ConnectionKey<ConnectionType>, rhs: ConnectionKey<ConnectionType>) -> Bool {
|
|
return ObjectIdentifier(lhs.connection) == ObjectIdentifier(rhs.connection)
|
|
}
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(ObjectIdentifier(self.connection))
|
|
}
|
|
|
|
func cancel() -> EventLoopFuture<Void> {
|
|
return self.connection.cancel()
|
|
}
|
|
}
|