Skip to content

Commit c65b2ae

Browse files
authored
Code clean up to to fix issue #234 - ConnectionsState exposes a setter into internal state (#310)
* 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.
1 parent f01021e commit c65b2ae

10 files changed

+605
-434
lines changed

Diff for: Sources/AsyncHTTPClient/Connection.swift

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the AsyncHTTPClient open source project
4+
//
5+
// Copyright (c) 2019-2020 Apple Inc. and the AsyncHTTPClient project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import Logging
17+
import NIO
18+
import NIOConcurrencyHelpers
19+
import NIOHTTP1
20+
import NIOHTTPCompression
21+
import NIOTLS
22+
import NIOTransportServices
23+
24+
/// A `Connection` represents a `Channel` in the context of the connection pool
25+
///
26+
/// In the `ConnectionPool`, each `Channel` belongs to a given `HTTP1ConnectionProvider`
27+
/// and has a certain "lease state" (see the `inUse` property).
28+
/// The role of `Connection` is to model this by storing a `Channel` alongside its associated properties
29+
/// so that they can be passed around together and correct provider can be identified when connection is released.
30+
class Connection {
31+
/// The provider this `Connection` belongs to.
32+
///
33+
/// This enables calling methods like `release()` directly on a `Connection` instead of
34+
/// calling `provider.release(connection)`. This gives a more object oriented feel to the API
35+
/// and can avoid having to keep explicit references to the pool at call site.
36+
private let provider: HTTP1ConnectionProvider
37+
38+
/// The `Channel` of this `Connection`
39+
///
40+
/// - Warning: Requests that lease connections from the `ConnectionPool` are responsible
41+
/// for removing the specific handlers they added to the `Channel` pipeline before releasing it to the pool.
42+
let channel: Channel
43+
44+
init(channel: Channel, provider: HTTP1ConnectionProvider) {
45+
self.channel = channel
46+
self.provider = provider
47+
}
48+
}
49+
50+
extension Connection {
51+
/// Release this `Connection` to its associated `HTTP1ConnectionProvider`.
52+
///
53+
/// - Warning: This only releases the connection and doesn't take care of cleaning handlers in the `Channel` pipeline.
54+
func release(closing: Bool, logger: Logger) {
55+
self.channel.eventLoop.assertInEventLoop()
56+
self.provider.release(connection: self, closing: closing, logger: logger)
57+
}
58+
59+
/// Called when channel exceeds idle time in pool.
60+
func timeout(logger: Logger) {
61+
self.channel.eventLoop.assertInEventLoop()
62+
self.provider.timeout(connection: self, logger: logger)
63+
}
64+
65+
/// Called when channel goes inactive while in the pool.
66+
func remoteClosed(logger: Logger) {
67+
self.channel.eventLoop.assertInEventLoop()
68+
self.provider.remoteClosed(connection: self, logger: logger)
69+
}
70+
71+
/// Called from `HTTP1ConnectionProvider.close` when client is shutting down.
72+
func close() -> EventLoopFuture<Void> {
73+
return self.channel.close()
74+
}
75+
}
76+
77+
/// Methods of Connection which are used in ConnectionsState extracted as protocol
78+
/// to facilitate test of ConnectionsState.
79+
protocol PoolManageableConnection: AnyObject {
80+
func cancel() -> EventLoopFuture<Void>
81+
var eventLoop: EventLoop { get }
82+
var isActiveEstimation: Bool { get }
83+
}
84+
85+
/// Implementation of methods used by ConnectionsState and its tests to manage Connection
86+
extension Connection: PoolManageableConnection {
87+
/// Convenience property indicating whether the underlying `Channel` is active or not.
88+
var isActiveEstimation: Bool {
89+
return self.channel.isActive
90+
}
91+
92+
var eventLoop: EventLoop {
93+
return self.channel.eventLoop
94+
}
95+
96+
func cancel() -> EventLoopFuture<Void> {
97+
return self.channel.triggerUserOutboundEvent(TaskCancelEvent())
98+
}
99+
}
100+
101+
extension Connection {
102+
/// Sets idle timeout handler and channel inactivity listener.
103+
func setIdleTimeout(timeout: TimeAmount?, logger: Logger) {
104+
_ = self.channel.pipeline.addHandler(IdleStateHandler(writeTimeout: timeout), position: .first).flatMap { _ in
105+
self.channel.pipeline.addHandler(IdlePoolConnectionHandler(connection: self, logger: logger))
106+
}
107+
}
108+
109+
/// Removes idle timeout handler and channel inactivity listener
110+
func cancelIdleTimeout() -> EventLoopFuture<Void> {
111+
return self.removeHandler(IdleStateHandler.self).flatMap { _ in
112+
self.removeHandler(IdlePoolConnectionHandler.self)
113+
}
114+
}
115+
}
116+
117+
class IdlePoolConnectionHandler: ChannelInboundHandler, RemovableChannelHandler {
118+
typealias InboundIn = NIOAny
119+
120+
let connection: Connection
121+
var eventSent: Bool
122+
let logger: Logger
123+
124+
init(connection: Connection, logger: Logger) {
125+
self.connection = connection
126+
self.eventSent = false
127+
self.logger = logger
128+
}
129+
130+
// this is needed to detect when remote end closes connection while connection is in the pool idling
131+
func channelInactive(context: ChannelHandlerContext) {
132+
if !self.eventSent {
133+
self.eventSent = true
134+
self.connection.remoteClosed(logger: self.logger)
135+
}
136+
}
137+
138+
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
139+
if let idleEvent = event as? IdleStateHandler.IdleStateEvent, idleEvent == .write {
140+
if !self.eventSent {
141+
self.eventSent = true
142+
self.connection.timeout(logger: self.logger)
143+
}
144+
} else {
145+
context.fireUserInboundEventTriggered(event)
146+
}
147+
}
148+
}
149+
150+
extension Connection: CustomStringConvertible {
151+
var description: String {
152+
return "\(self.channel)"
153+
}
154+
}
155+
156+
struct ConnectionKey<ConnectionType>: Hashable where ConnectionType: PoolManageableConnection {
157+
let connection: ConnectionType
158+
159+
init(_ connection: ConnectionType) {
160+
self.connection = connection
161+
}
162+
163+
static func == (lhs: ConnectionKey<ConnectionType>, rhs: ConnectionKey<ConnectionType>) -> Bool {
164+
return ObjectIdentifier(lhs.connection) == ObjectIdentifier(rhs.connection)
165+
}
166+
167+
func hash(into hasher: inout Hasher) {
168+
hasher.combine(ObjectIdentifier(self.connection))
169+
}
170+
171+
func cancel() -> EventLoopFuture<Void> {
172+
return self.connection.cancel()
173+
}
174+
}

Diff for: Sources/AsyncHTTPClient/ConnectionPool.swift

+7-135
Original file line numberDiff line numberDiff line change
@@ -163,101 +163,6 @@ final class ConnectionPool {
163163
}
164164
}
165165

166-
/// A `Connection` represents a `Channel` in the context of the connection pool
167-
///
168-
/// In the `ConnectionPool`, each `Channel` belongs to a given `HTTP1ConnectionProvider`
169-
/// and has a certain "lease state" (see the `inUse` property).
170-
/// The role of `Connection` is to model this by storing a `Channel` alongside its associated properties
171-
/// so that they can be passed around together and correct provider can be identified when connection is released.
172-
class Connection {
173-
/// The provider this `Connection` belongs to.
174-
///
175-
/// This enables calling methods like `release()` directly on a `Connection` instead of
176-
/// calling `provider.release(connection)`. This gives a more object oriented feel to the API
177-
/// and can avoid having to keep explicit references to the pool at call site.
178-
let provider: HTTP1ConnectionProvider
179-
180-
/// The `Channel` of this `Connection`
181-
///
182-
/// - Warning: Requests that lease connections from the `ConnectionPool` are responsible
183-
/// for removing the specific handlers they added to the `Channel` pipeline before releasing it to the pool.
184-
let channel: Channel
185-
186-
init(channel: Channel, provider: HTTP1ConnectionProvider) {
187-
self.channel = channel
188-
self.provider = provider
189-
}
190-
191-
/// Convenience property indicating wether the underlying `Channel` is active or not.
192-
var isActiveEstimation: Bool {
193-
return self.channel.isActive
194-
}
195-
196-
/// Release this `Connection` to its associated `HTTP1ConnectionProvider`.
197-
///
198-
/// - Warning: This only releases the connection and doesn't take care of cleaning handlers in the `Channel` pipeline.
199-
func release(closing: Bool, logger: Logger) {
200-
self.channel.eventLoop.assertInEventLoop()
201-
self.provider.release(connection: self, closing: closing, logger: logger)
202-
}
203-
204-
/// Called when channel exceeds idle time in pool.
205-
func timeout(logger: Logger) {
206-
self.channel.eventLoop.assertInEventLoop()
207-
self.provider.timeout(connection: self, logger: logger)
208-
}
209-
210-
/// Called when channel goes inactive while in the pool.
211-
func remoteClosed(logger: Logger) {
212-
self.channel.eventLoop.assertInEventLoop()
213-
self.provider.remoteClosed(connection: self, logger: logger)
214-
}
215-
216-
func cancel() -> EventLoopFuture<Void> {
217-
return self.channel.triggerUserOutboundEvent(TaskCancelEvent())
218-
}
219-
220-
/// Called from `HTTP1ConnectionProvider.close` when client is shutting down.
221-
func close() -> EventLoopFuture<Void> {
222-
return self.channel.close()
223-
}
224-
225-
/// Sets idle timeout handler and channel inactivity listener.
226-
func setIdleTimeout(timeout: TimeAmount?, logger: Logger) {
227-
_ = self.channel.pipeline.addHandler(IdleStateHandler(writeTimeout: timeout), position: .first).flatMap { _ in
228-
self.channel.pipeline.addHandler(IdlePoolConnectionHandler(connection: self,
229-
logger: logger))
230-
}
231-
}
232-
233-
/// Removes idle timeout handler and channel inactivity listener
234-
func cancelIdleTimeout() -> EventLoopFuture<Void> {
235-
return self.removeHandler(IdleStateHandler.self).flatMap { _ in
236-
self.removeHandler(IdlePoolConnectionHandler.self)
237-
}
238-
}
239-
}
240-
241-
struct ConnectionKey: Hashable {
242-
let connection: Connection
243-
244-
init(_ connection: Connection) {
245-
self.connection = connection
246-
}
247-
248-
static func == (lhs: ConnectionKey, rhs: ConnectionKey) -> Bool {
249-
return ObjectIdentifier(lhs.connection) == ObjectIdentifier(rhs.connection)
250-
}
251-
252-
func hash(into hasher: inout Hasher) {
253-
hasher.combine(ObjectIdentifier(self.connection))
254-
}
255-
256-
func cancel() -> EventLoopFuture<Void> {
257-
return self.connection.cancel()
258-
}
259-
}
260-
261166
/// A connection provider of `HTTP/1.1` connections with a given `Key` (host, scheme, port)
262167
///
263168
/// On top of enabling connection reuse this provider it also facilitates the creation
@@ -286,7 +191,7 @@ class HTTP1ConnectionProvider {
286191

287192
var closePromise: EventLoopPromise<Void>
288193

289-
var state: ConnectionsState
194+
var state: ConnectionsState<Connection>
290195

291196
private let backgroundActivityLogger: Logger
292197

@@ -317,7 +222,7 @@ class HTTP1ConnectionProvider {
317222
self.state.assertInvariants()
318223
}
319224

320-
func execute(_ action: Action, logger: Logger) {
225+
func execute(_ action: Action<Connection>, logger: Logger) {
321226
switch action {
322227
case .lease(let connection, let waiter):
323228
// if connection is became inactive, we create a new one.
@@ -392,7 +297,7 @@ class HTTP1ConnectionProvider {
392297
func getConnection(preference: HTTPClient.EventLoopPreference,
393298
setupComplete: EventLoopFuture<Void>,
394299
logger: Logger) -> EventLoopFuture<Connection> {
395-
let waiter = Waiter(promise: self.eventLoop.makePromise(), setupComplete: setupComplete, preference: preference)
300+
let waiter = Waiter<Connection>(promise: self.eventLoop.makePromise(), setupComplete: setupComplete, preference: preference)
396301

397302
let action: Action = self.lock.withLock {
398303
self.state.acquire(waiter: waiter)
@@ -404,10 +309,10 @@ class HTTP1ConnectionProvider {
404309
}
405310

406311
func connect(_ result: Result<Channel, Error>,
407-
waiter: Waiter,
312+
waiter: Waiter<Connection>,
408313
replacing closedConnection: Connection? = nil,
409314
logger: Logger) {
410-
let action: Action
315+
let action: Action<Connection>
411316
switch result {
412317
case .success(let channel):
413318
logger.trace("successfully created connection",
@@ -573,9 +478,9 @@ class HTTP1ConnectionProvider {
573478
///
574479
/// `Waiter`s are created when `maximumConcurrentConnections` is reached
575480
/// and we cannot create new connections anymore.
576-
struct Waiter {
481+
struct Waiter<ConnectionType: PoolManageableConnection> {
577482
/// The promise to complete once a connection is available
578-
let promise: EventLoopPromise<Connection>
483+
let promise: EventLoopPromise<ConnectionType>
579484

580485
/// Future that will be succeeded when request timeout handler and `TaskHandler` are added to the pipeline.
581486
let setupComplete: EventLoopFuture<Void>
@@ -586,39 +491,6 @@ class HTTP1ConnectionProvider {
586491
}
587492
}
588493

589-
class IdlePoolConnectionHandler: ChannelInboundHandler, RemovableChannelHandler {
590-
typealias InboundIn = NIOAny
591-
592-
let connection: Connection
593-
var eventSent: Bool
594-
let logger: Logger
595-
596-
init(connection: Connection, logger: Logger) {
597-
self.connection = connection
598-
self.eventSent = false
599-
self.logger = logger
600-
}
601-
602-
// this is needed to detect when remote end closes connection while connection is in the pool idling
603-
func channelInactive(context: ChannelHandlerContext) {
604-
if !self.eventSent {
605-
self.eventSent = true
606-
self.connection.remoteClosed(logger: self.logger)
607-
}
608-
}
609-
610-
func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
611-
if let idleEvent = event as? IdleStateHandler.IdleStateEvent, idleEvent == .write {
612-
if !self.eventSent {
613-
self.eventSent = true
614-
self.connection.timeout(logger: self.logger)
615-
}
616-
} else {
617-
context.fireUserInboundEventTriggered(event)
618-
}
619-
}
620-
}
621-
622494
extension CircularBuffer {
623495
mutating func swap(at index: Index, with value: Element) -> Element {
624496
let tmp = self[index]

0 commit comments

Comments
 (0)