Skip to content

Commit e401a28

Browse files
authored
Fixes #234 by removing setter on internal ConnectionsState so modification (#311)
allowed only using exposed API. Motivation: Having a setter for internal state of ConnectionsState led to a subset of test testing invalid invariants, for example when we have at the same time an available connecion and a waiter, which is an invalid state of the system. Modifications: * test of ConnectionsState * ConnectionsState * ConnectionPool are modified so the state under tests is achieved only by a sequence of modifications invoked by state API. During modification some tests are eliminated as they were testing artificial state, which can not be achieved by exposed APIs. ConnectionsState is pruned from "replace" as in no valid state we can have a situation when we can "replace" a connection. Invalid invariants and tests are removed. Result: We do not have a way to modify state of the ConnectionsState by direct interaction with private state of the object. Getter on the state is considered harmless and used for tests only.
1 parent c65b2ae commit e401a28

File tree

6 files changed

+282
-1066
lines changed

6 files changed

+282
-1066
lines changed

Diff for: Sources/AsyncHTTPClient/ConnectionPool.swift

+6-64
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ class HTTP1ConnectionProvider {
235235
logger.trace("opening fresh connection (found matching but inactive connection)",
236236
metadata: ["ahc-dead-connection": "\(connection)"])
237237
self.makeChannel(preference: waiter.preference).whenComplete { result in
238-
self.connect(result, waiter: waiter, replacing: connection, logger: logger)
238+
self.connect(result, waiter: waiter, logger: logger)
239239
}
240240
}
241241
}
@@ -252,7 +252,7 @@ class HTTP1ConnectionProvider {
252252
metadata: ["ahc-old-connection": "\(connection)",
253253
"ahc-waiter": "\(waiter)"])
254254
self.makeChannel(preference: waiter.preference).whenComplete { result in
255-
self.connect(result, waiter: waiter, replacing: connection, logger: logger)
255+
self.connect(result, waiter: waiter, logger: logger)
256256
}
257257
}
258258
case .park(let connection):
@@ -308,21 +308,15 @@ class HTTP1ConnectionProvider {
308308
return waiter.promise.futureResult
309309
}
310310

311-
func connect(_ result: Result<Channel, Error>,
312-
waiter: Waiter<Connection>,
313-
replacing closedConnection: Connection? = nil,
314-
logger: Logger) {
311+
func connect(_ result: Result<Channel, Error>, waiter: Waiter<Connection>, logger: Logger) {
315312
let action: Action<Connection>
316313
switch result {
317314
case .success(let channel):
318315
logger.trace("successfully created connection",
319316
metadata: ["ahc-connection": "\(channel)"])
320317
let connection = Connection(channel: channel, provider: self)
321318
action = self.lock.withLock {
322-
if let closedConnection = closedConnection {
323-
self.state.drop(connection: closedConnection)
324-
}
325-
return self.state.offer(connection: connection)
319+
self.state.offer(connection: connection)
326320
}
327321

328322
switch action {
@@ -367,7 +361,7 @@ class HTTP1ConnectionProvider {
367361
// This is needed to start a new stack, otherwise, since this is called on a previous
368362
// future completion handler chain, it will be growing indefinitely until the connection is closed.
369363
// We might revisit this when https://github.com/apple/swift-nio/issues/970 is resolved.
370-
connection.channel.eventLoop.execute {
364+
connection.eventLoop.execute {
371365
self.execute(action, logger: logger)
372366
}
373367
}
@@ -418,59 +412,7 @@ class HTTP1ConnectionProvider {
418412
}
419413

420414
private func makeChannel(preference: HTTPClient.EventLoopPreference) -> EventLoopFuture<Channel> {
421-
let channelEventLoop = preference.bestEventLoop ?? self.eventLoop
422-
let requiresTLS = self.key.scheme.requiresTLS
423-
let bootstrap: NIOClientTCPBootstrap
424-
do {
425-
bootstrap = try NIOClientTCPBootstrap.makeHTTPClientBootstrapBase(on: channelEventLoop, host: self.key.host, port: self.key.port, requiresTLS: requiresTLS, configuration: self.configuration)
426-
} catch {
427-
return channelEventLoop.makeFailedFuture(error)
428-
}
429-
430-
let channel: EventLoopFuture<Channel>
431-
switch self.key.scheme {
432-
case .http, .https:
433-
let address = HTTPClient.resolveAddress(host: self.key.host, port: self.key.port, proxy: self.configuration.proxy)
434-
channel = bootstrap.connect(host: address.host, port: address.port)
435-
case .unix, .http_unix, .https_unix:
436-
channel = bootstrap.connect(unixDomainSocketPath: self.key.unixPath)
437-
}
438-
439-
return channel.flatMap { channel in
440-
let requiresSSLHandler = self.configuration.proxy != nil && self.key.scheme.requiresTLS
441-
let handshakePromise = channel.eventLoop.makePromise(of: Void.self)
442-
443-
channel.pipeline.addSSLHandlerIfNeeded(for: self.key, tlsConfiguration: self.configuration.tlsConfiguration, addSSLClient: requiresSSLHandler, handshakePromise: handshakePromise)
444-
445-
return handshakePromise.futureResult.flatMap {
446-
channel.pipeline.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
447-
}.flatMap {
448-
#if canImport(Network)
449-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
450-
return channel.pipeline.addHandler(HTTPClient.NWErrorHandler(), position: .first)
451-
}
452-
#endif
453-
return channel.eventLoop.makeSucceededFuture(())
454-
}.flatMap {
455-
switch self.configuration.decompression {
456-
case .disabled:
457-
return channel.eventLoop.makeSucceededFuture(())
458-
case .enabled(let limit):
459-
let decompressHandler = NIOHTTPResponseDecompressor(limit: limit)
460-
return channel.pipeline.addHandler(decompressHandler)
461-
}
462-
}.map {
463-
channel
464-
}
465-
}.flatMapError { error in
466-
#if canImport(Network)
467-
var error = error
468-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
469-
error = HTTPClient.NWErrorHandler.translateError(error)
470-
}
471-
#endif
472-
return channelEventLoop.makeFailedFuture(error)
473-
}
415+
return NIOClientTCPBootstrap.makeHTTP1Channel(destination: self.key, eventLoop: self.eventLoop, configuration: self.configuration, preference: preference)
474416
}
475417

476418
/// A `Waiter` represents a request that waits for a connection when none is

Diff for: Sources/AsyncHTTPClient/ConnectionsState.swift

+27-48
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,6 @@ extension HTTP1ConnectionProvider {
7474
return Snapshot(state: self.state, availableConnections: self.availableConnections, leasedConnections: self.leasedConnections, waiters: self.waiters, openedConnectionsCount: self.openedConnectionsCount, pending: self.pending)
7575
}
7676

77-
mutating func testsOnly_setInternalState(_ snapshot: Snapshot<ConnectionType>) {
78-
self.state = snapshot.state
79-
self.availableConnections = snapshot.availableConnections
80-
self.leasedConnections = snapshot.leasedConnections
81-
self.waiters = snapshot.waiters
82-
self.openedConnectionsCount = snapshot.openedConnectionsCount
83-
self.pending = snapshot.pending
84-
}
85-
8677
func assertInvariants() {
8778
assert(self.waiters.isEmpty)
8879
assert(self.availableConnections.isEmpty)
@@ -158,30 +149,22 @@ extension HTTP1ConnectionProvider {
158149

159150
if connection.isActiveEstimation, !closing { // If connection is alive, we can offer it to a next waiter
160151
if let waiter = self.waiters.popFirst() {
152+
// There should be no case where we have both capacity and a waiter here.
153+
// Waiter can only exists if there was no capacity at aquire. If some connection
154+
// is released when we have waiter it can only indicate that we should lease (if EL are the same),
155+
// or replace (if they are different). But we cannot increase connection count here.
156+
assert(!self.hasCapacity)
157+
161158
let (eventLoop, required) = self.resolvePreference(waiter.preference)
162159

163160
// If returned connection is on same EL or we do not require special EL - lease it
164161
if connection.eventLoop === eventLoop || !required {
165162
return .lease(connection, waiter)
166163
}
167164

168-
// If there is an opened connection on the same loop, lease it and park returned
169-
if let found = self.availableConnections.firstIndex(where: { $0.eventLoop === eventLoop }) {
170-
self.leasedConnections.remove(ConnectionKey(connection))
171-
let replacement = self.availableConnections.swap(at: found, with: connection)
172-
self.leasedConnections.insert(ConnectionKey(replacement))
173-
return .parkAnd(connection, .lease(replacement, waiter))
174-
}
175-
176-
// If we can create new connection - do it
177-
if self.hasCapacity {
178-
self.leasedConnections.remove(ConnectionKey(connection))
179-
self.availableConnections.append(connection)
180-
self.openedConnectionsCount += 1
181-
return .parkAnd(connection, .create(waiter))
182-
}
183-
184165
// If we cannot create new connections, we will have to replace returned connection with a new one on the required loop
166+
// We will keep the `openedConnectionCount`, since .replace === .create, so we decrease and increase the `openedConnectionCount`
167+
self.leasedConnections.remove(ConnectionKey(connection))
185168
return .replace(connection, waiter)
186169
} else { // or park, if there are no waiters
187170
self.leasedConnections.remove(ConnectionKey(connection))
@@ -214,15 +197,6 @@ extension HTTP1ConnectionProvider {
214197
}
215198
}
216199

217-
mutating func drop(connection: ConnectionType) {
218-
switch self.state {
219-
case .active:
220-
self.leasedConnections.remove(ConnectionKey(connection))
221-
case .closed:
222-
assertionFailure("should not happen")
223-
}
224-
}
225-
226200
mutating func connectFailed() -> Action<ConnectionType> {
227201
switch self.state {
228202
case .active:
@@ -287,20 +261,25 @@ extension HTTP1ConnectionProvider {
287261

288262
mutating func processNextWaiter() -> Action<ConnectionType> {
289263
if let waiter = self.waiters.popFirst() {
290-
let (eventLoop, required) = self.resolvePreference(waiter.preference)
291-
292-
// If specific EL is required, we have only two options - find open one or create a new one
293-
if required, let found = self.availableConnections.firstIndex(where: { $0.eventLoop === eventLoop }) {
294-
let connection = self.availableConnections.remove(at: found)
295-
self.leasedConnections.insert(ConnectionKey(connection))
296-
return .lease(connection, waiter)
297-
} else if !required, let connection = self.availableConnections.popFirst() {
298-
self.leasedConnections.insert(ConnectionKey(connection))
299-
return .lease(connection, waiter)
300-
} else {
301-
self.openedConnectionsCount += 1
302-
return .create(waiter)
303-
}
264+
// There should be no case where we have waiters and available connections at the same time.
265+
//
266+
// This method is called in following cases:
267+
//
268+
// 1. from `release` when connection is inactive and cannot be re-used
269+
// 2. from `connectFailed` when we failed to establish a new connection
270+
// 3. from `remoteClose` when connection was closed by the remote side and cannot be re-used
271+
// 4. from `timeout` when connection was closed due to idle timeout and cannot be re-used.
272+
//
273+
// In all cases connection, which triggered the transition, will not be in `available` state.
274+
//
275+
// Given that the waiter can only be present in the pool if there were no available connections
276+
// (otherwise it had been leased a connection immediately on getting the connection), we do not
277+
// see a situation when we can lease another available connection, therefore the only course
278+
// of action is to create a new connection for the waiter.
279+
assert(self.availableConnections.isEmpty)
280+
281+
self.openedConnectionsCount += 1
282+
return .create(waiter)
304283
}
305284

306285
// if capacity is at max and the are no waiters and no in-flight requests for connection, we are closing this provider

Diff for: Sources/AsyncHTTPClient/Utils.swift

+59
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,65 @@ extension NIOClientTCPBootstrap {
138138
return channelAddedFuture
139139
}
140140
}
141+
142+
static func makeHTTP1Channel(destination: ConnectionPool.Key, eventLoop: EventLoop, configuration: HTTPClient.Configuration, preference: HTTPClient.EventLoopPreference) -> EventLoopFuture<Channel> {
143+
let channelEventLoop = preference.bestEventLoop ?? eventLoop
144+
145+
let key = destination
146+
147+
let requiresTLS = key.scheme.requiresTLS
148+
let bootstrap: NIOClientTCPBootstrap
149+
do {
150+
bootstrap = try NIOClientTCPBootstrap.makeHTTPClientBootstrapBase(on: channelEventLoop, host: key.host, port: key.port, requiresTLS: requiresTLS, configuration: configuration)
151+
} catch {
152+
return channelEventLoop.makeFailedFuture(error)
153+
}
154+
155+
let channel: EventLoopFuture<Channel>
156+
switch key.scheme {
157+
case .http, .https:
158+
let address = HTTPClient.resolveAddress(host: key.host, port: key.port, proxy: configuration.proxy)
159+
channel = bootstrap.connect(host: address.host, port: address.port)
160+
case .unix, .http_unix, .https_unix:
161+
channel = bootstrap.connect(unixDomainSocketPath: key.unixPath)
162+
}
163+
164+
return channel.flatMap { channel in
165+
let requiresSSLHandler = configuration.proxy != nil && key.scheme.requiresTLS
166+
let handshakePromise = channel.eventLoop.makePromise(of: Void.self)
167+
168+
channel.pipeline.addSSLHandlerIfNeeded(for: key, tlsConfiguration: configuration.tlsConfiguration, addSSLClient: requiresSSLHandler, handshakePromise: handshakePromise)
169+
170+
return handshakePromise.futureResult.flatMap {
171+
channel.pipeline.addHTTPClientHandlers(leftOverBytesStrategy: .forwardBytes)
172+
}.flatMap {
173+
#if canImport(Network)
174+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
175+
return channel.pipeline.addHandler(HTTPClient.NWErrorHandler(), position: .first)
176+
}
177+
#endif
178+
return channel.eventLoop.makeSucceededFuture(())
179+
}.flatMap {
180+
switch configuration.decompression {
181+
case .disabled:
182+
return channel.eventLoop.makeSucceededFuture(())
183+
case .enabled(let limit):
184+
let decompressHandler = NIOHTTPResponseDecompressor(limit: limit)
185+
return channel.pipeline.addHandler(decompressHandler)
186+
}
187+
}.map {
188+
channel
189+
}
190+
}.flatMapError { error in
191+
#if canImport(Network)
192+
var error = error
193+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), bootstrap.underlyingBootstrap is NIOTSConnectionBootstrap {
194+
error = HTTPClient.NWErrorHandler.translateError(error)
195+
}
196+
#endif
197+
return channelEventLoop.makeFailedFuture(error)
198+
}
199+
}
141200
}
142201

143202
extension Connection {

Diff for: Tests/AsyncHTTPClientTests/ConnectionPoolTests+XCTest.swift

-8
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,10 @@ extension ConnectionPoolTests {
4141
("testReleaseInactiveConnectionEmptyQueueHasConnections", testReleaseInactiveConnectionEmptyQueueHasConnections),
4242
("testReleaseAliveConnectionHasWaiter", testReleaseAliveConnectionHasWaiter),
4343
("testReleaseInactiveConnectionHasWaitersNoConnections", testReleaseInactiveConnectionHasWaitersNoConnections),
44-
("testReleaseInactiveConnectionHasWaitersHasConnections", testReleaseInactiveConnectionHasWaitersHasConnections),
4544
("testReleaseAliveConnectionSameELHasWaiterSpecificEL", testReleaseAliveConnectionSameELHasWaiterSpecificEL),
46-
("testReleaseAliveConnectionDifferentELNoSameELConnectionsHasWaiterSpecificEL", testReleaseAliveConnectionDifferentELNoSameELConnectionsHasWaiterSpecificEL),
47-
("testReleaseAliveConnectionDifferentELHasSameELConnectionsHasWaiterSpecificEL", testReleaseAliveConnectionDifferentELHasSameELConnectionsHasWaiterSpecificEL),
4845
("testReleaseAliveConnectionDifferentELNoSameELConnectionsOnLimitHasWaiterSpecificEL", testReleaseAliveConnectionDifferentELNoSameELConnectionsOnLimitHasWaiterSpecificEL),
49-
("testReleaseInactiveConnectionHasWaitersHasSameELConnectionsSpecificEL", testReleaseInactiveConnectionHasWaitersHasSameELConnectionsSpecificEL),
50-
("testReleaseInactiveConnectionHasWaitersNoSameELConnectionsSpecificEL", testReleaseInactiveConnectionHasWaitersNoSameELConnectionsSpecificEL),
5146
("testNextWaiterEmptyQueue", testNextWaiterEmptyQueue),
5247
("testNextWaiterEmptyQueueHasConnections", testNextWaiterEmptyQueueHasConnections),
53-
("testNextWaiterHasWaitersHasConnections", testNextWaiterHasWaitersHasConnections),
54-
("testNextWaiterHasWaitersHasSameELConnectionsSpecificEL", testNextWaiterHasWaitersHasSameELConnectionsSpecificEL),
55-
("testNextWaiterHasWaitersHasDifferentELConnectionsSpecificEL", testNextWaiterHasWaitersHasDifferentELConnectionsSpecificEL),
5648
("testTimeoutLeasedConnection", testTimeoutLeasedConnection),
5749
("testTimeoutAvailableConnection", testTimeoutAvailableConnection),
5850
("testRemoteClosedLeasedConnection", testRemoteClosedLeasedConnection),

0 commit comments

Comments
 (0)