Skip to content

Commit ae742aa

Browse files
committed
Started work on h2
1 parent 5cfdae6 commit ae742aa

10 files changed

+690
-507
lines changed

Diff for: Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Manager.swift

+152
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,159 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import Logging
16+
import NIO
1517
import NIOConcurrencyHelpers
18+
import NIOHTTP1
19+
20+
protocol HTTPConnectionPoolManagerDelegate: AnyObject {
21+
func httpConnectionPoolManagerDidShutdown(_: HTTPConnectionPool.Manager, unclean: Bool)
22+
}
23+
24+
extension HTTPConnectionPool {
25+
final class Manager {
26+
private typealias Key = ConnectionPool.Key
27+
28+
private var _pools: [Key: HTTPConnectionPool] = [:]
29+
private let lock = Lock()
30+
31+
private let sslContextCache = SSLContextCache()
32+
33+
enum State {
34+
case active
35+
case shuttingDown(unclean: Bool)
36+
case shutDown
37+
}
38+
39+
let eventLoopGroup: EventLoopGroup
40+
let configuration: HTTPClient.Configuration
41+
let connectionIDGenerator = Connection.ID.globalGenerator
42+
let logger: Logger
43+
44+
/// A delegate to inform about the pools managers shutdown
45+
///
46+
/// NOTE: Normally we create retain cycles in SwiftNIO code that we break on shutdown. However we wan't to inform
47+
/// users that they must call `shutdown` on their AsyncHTTPClient. The best way to make them aware is with
48+
/// a `preconditionFailure` in the HTTPClient's `deinit`. If we create a retain cycle here, the
49+
/// `HTTPClient`'s `deinit` can never be reached. Instead the `HTTPClient` would leak.
50+
///
51+
/// The delegate is not thread safe at all. This only works if the HTTPClient sets itself as a delegate in its own
52+
/// init.
53+
weak var delegate: HTTPConnectionPoolManagerDelegate?
54+
55+
private var state: State = .active
56+
57+
init(eventLoopGroup: EventLoopGroup,
58+
configuration: HTTPClient.Configuration,
59+
backgroundActivityLogger logger: Logger) {
60+
self.eventLoopGroup = eventLoopGroup
61+
self.configuration = configuration
62+
self.logger = logger
63+
}
64+
65+
deinit {
66+
guard case .shutDown = self.state else {
67+
preconditionFailure("Manager must be shutdown before deinit")
68+
}
69+
}
70+
71+
func execute(request: HTTPRequestTask) {
72+
let key = Key(request.request)
73+
74+
let poolResult = self.lock.withLock { () -> Result<HTTPConnectionPool, HTTPClientError> in
75+
guard case .active = self.state else {
76+
return .failure(HTTPClientError.alreadyShutdown)
77+
}
78+
79+
if let pool = self._pools[key] {
80+
return .success(pool)
81+
}
82+
83+
let pool = HTTPConnectionPool(
84+
eventLoopGroup: self.eventLoopGroup,
85+
sslContextCache: self.sslContextCache,
86+
tlsConfiguration: request.request.tlsConfiguration,
87+
clientConfiguration: self.configuration,
88+
key: key,
89+
delegate: self,
90+
idGenerator: self.connectionIDGenerator,
91+
logger: self.logger
92+
)
93+
self._pools[key] = pool
94+
return .success(pool)
95+
}
96+
97+
switch poolResult {
98+
case .success(let pool):
99+
pool.execute(request: request)
100+
case .failure(let error):
101+
request.fail(error)
102+
}
103+
}
104+
105+
func shutdown() {
106+
let pools = self.lock.withLock { () -> [Key: HTTPConnectionPool] in
107+
guard case .active = self.state else {
108+
preconditionFailure("PoolManager already shutdown")
109+
}
110+
111+
// If there aren't any pools, we can mark the pool as shut down right away.
112+
if self._pools.isEmpty {
113+
self.state = .shutDown
114+
} else {
115+
self.state = .shuttingDown(unclean: false)
116+
}
117+
118+
return self._pools
119+
}
120+
121+
// if no pools are returned, the manager is already shutdown completely. Inform the
122+
// delegate. This is a very clean shutdown...
123+
if pools.isEmpty {
124+
self.delegate?.httpConnectionPoolManagerDidShutdown(self, unclean: false)
125+
return
126+
}
127+
128+
pools.values.forEach { pool in
129+
pool.shutdown()
130+
}
131+
}
132+
}
133+
}
134+
135+
extension HTTPConnectionPool.Manager: HTTPConnectionPoolDelegate {
136+
enum CloseAction {
137+
case close(unclean: Bool)
138+
case wait
139+
}
140+
141+
func connectionPoolDidShutdown(_ pool: HTTPConnectionPool, unclean: Bool) {
142+
let closeAction = self.lock.withLock { () -> CloseAction in
143+
guard case .shuttingDown(let soFarUnclean) = self.state else {
144+
preconditionFailure("Why are pools shutting down, if the manager did not give a signal")
145+
}
146+
147+
guard self._pools.removeValue(forKey: pool.key) === pool else {
148+
preconditionFailure("Expected that the pool was ")
149+
}
150+
151+
if self._pools.isEmpty {
152+
self.state = .shutDown
153+
return .close(unclean: soFarUnclean || unclean)
154+
} else {
155+
self.state = .shuttingDown(unclean: soFarUnclean || unclean)
156+
return .wait
157+
}
158+
}
159+
160+
switch closeAction {
161+
case .close(unclean: let unclean):
162+
self.delegate?.httpConnectionPoolManagerDidShutdown(self, unclean: unclean)
163+
case .wait:
164+
break
165+
}
166+
}
167+
}
16168

17169
extension HTTPConnectionPool.Connection.ID {
18170
static var globalGenerator = Generator()

Diff for: Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool.swift

-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ protocol HTTPConnectionPoolDelegate {
2929
}
3030

3131
class HTTPConnectionPool {
32-
3332
struct Connection: Equatable {
3433
typealias ID = Int
3534

Diff for: Sources/AsyncHTTPClient/HTTPClient.swift

+53-94
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public class HTTPClient {
6666
public let eventLoopGroup: EventLoopGroup
6767
let eventLoopGroupProvider: EventLoopGroupProvider
6868
let configuration: Configuration
69-
let pool: ConnectionPool
69+
let poolManager: HTTPConnectionPool.Manager
7070
var state: State
7171
private let stateLock = Lock()
7272

@@ -108,14 +108,20 @@ public class HTTPClient {
108108
#endif
109109
}
110110
self.configuration = configuration
111-
self.pool = ConnectionPool(configuration: configuration,
112-
backgroundActivityLogger: backgroundActivityLogger)
111+
self.poolManager = HTTPConnectionPool.Manager(
112+
eventLoopGroup: self.eventLoopGroup,
113+
configuration: self.configuration,
114+
backgroundActivityLogger: backgroundActivityLogger
115+
)
113116
self.state = .upAndRunning
117+
118+
self.poolManager.delegate = self
114119
}
115120

116121
deinit {
117-
assert(self.pool.count == 0)
118-
assert(self.state == .shutDown, "Client not shut down before the deinit. Please call client.syncShutdown() when no longer needed.")
122+
guard case .shutDown = self.state else {
123+
preconditionFailure("Client not shut down before the deinit. Please call client.syncShutdown() when no longer needed.")
124+
}
119125
}
120126

121127
/// Shuts down the client and `EventLoopGroup` if it was created by the client.
@@ -189,36 +195,17 @@ public class HTTPClient {
189195
private func shutdown(requiresCleanClose: Bool, queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
190196
do {
191197
try self.stateLock.withLock {
192-
if self.state != .upAndRunning {
198+
guard case .upAndRunning = self.state else {
193199
throw HTTPClientError.alreadyShutdown
194200
}
195-
self.state = .shuttingDown
201+
self.state = .shuttingDown(requiresCleanClose: requiresCleanClose, callback: callback)
196202
}
197203
} catch {
198204
callback(error)
199205
return
200206
}
201207

202-
self.pool.close(on: self.eventLoopGroup.next()).whenComplete { result in
203-
var closeError: Error?
204-
switch result {
205-
case .failure(let error):
206-
closeError = error
207-
case .success(let cleanShutdown):
208-
if !cleanShutdown, requiresCleanClose {
209-
closeError = HTTPClientError.uncleanShutdown
210-
}
211-
212-
self.shutdownEventLoop(queue: queue) { eventLoopError in
213-
// we prioritise .uncleanShutdown here
214-
if let error = closeError {
215-
callback(error)
216-
} else {
217-
callback(eventLoopError)
218-
}
219-
}
220-
}
221-
}
208+
self.poolManager.shutdown()
222209
}
223210

224211
/// Execute `GET` request using specified URL.
@@ -490,7 +477,7 @@ public class HTTPClient {
490477
let taskEL: EventLoop
491478
switch eventLoopPreference.preference {
492479
case .indifferent:
493-
taskEL = self.pool.associatedEventLoop(for: ConnectionPool.Key(request)) ?? self.eventLoopGroup.next()
480+
taskEL = self.eventLoopGroup.next()
494481
case .delegate(on: let eventLoop):
495482
precondition(self.eventLoopGroup.makeIterator().contains { $0 === eventLoop }, "Provided EventLoop must be part of clients EventLoopGroup.")
496483
taskEL = eventLoop
@@ -538,77 +525,30 @@ public class HTTPClient {
538525
}
539526

540527
let task = Task<Delegate.Response>(eventLoop: taskEL, logger: logger)
541-
let setupComplete = taskEL.makePromise(of: Void.self)
542-
let connection = self.pool.getConnection(request,
543-
preference: eventLoopPreference,
544-
taskEventLoop: taskEL,
545-
deadline: deadline,
546-
setupComplete: setupComplete.futureResult,
547-
logger: logger)
548-
549-
let taskHandler = TaskHandler(task: task,
550-
kind: request.kind,
551-
delegate: delegate,
552-
redirectHandler: redirectHandler,
553-
ignoreUncleanSSLShutdown: self.configuration.ignoreUncleanSSLShutdown,
554-
logger: logger)
555-
556-
connection.flatMap { connection -> EventLoopFuture<Void> in
557-
logger.debug("got connection for request",
558-
metadata: ["ahc-connection": "\(connection)",
559-
"ahc-request": "\(request.method) \(request.url)",
560-
"ahc-channel-el": "\(connection.channel.eventLoop)",
561-
"ahc-task-el": "\(taskEL)"])
562-
563-
let channel = connection.channel
564-
565-
func prepareChannelForTask0() -> EventLoopFuture<Void> {
566-
do {
567-
let syncPipelineOperations = channel.pipeline.syncOperations
568-
569-
if let timeout = self.resolve(timeout: self.configuration.timeout.read, deadline: deadline) {
570-
try syncPipelineOperations.addHandler(IdleStateHandler(readTimeout: timeout))
571-
}
572-
573-
try syncPipelineOperations.addHandler(taskHandler)
574-
} catch {
575-
connection.release(closing: true, logger: logger)
576-
return channel.eventLoop.makeFailedFuture(error)
577-
}
578-
579-
task.setConnection(connection)
580528

581-
let isCancelled = task.lock.withLock {
582-
task.cancelled
583-
}
584-
585-
if !isCancelled {
586-
return channel.writeAndFlush(request).flatMapError { _ in
587-
// At this point the `TaskHandler` will already be present
588-
// to handle the failure and pass it to the `promise`
589-
channel.eventLoop.makeSucceededVoidFuture()
590-
}
591-
} else {
592-
return channel.eventLoop.makeSucceededVoidFuture()
593-
}
529+
let requestBag = RequestBag(
530+
request: request,
531+
eventLoopPreference: eventLoopPreference,
532+
task: task,
533+
redirectHandler: redirectHandler,
534+
connectionDeadline: .now() + (self.configuration.timeout.connect ?? .seconds(10)),
535+
idleReadTimeout: self.configuration.timeout.read,
536+
delegate: delegate
537+
)
538+
539+
var deadlineSchedule: Scheduled<Void>?
540+
if let deadline = deadline {
541+
deadlineSchedule = taskEL.scheduleTask(deadline: deadline) {
542+
requestBag.fail(HTTPClientError.deadlineExceeded)
594543
}
595544

596-
if channel.eventLoop.inEventLoop {
597-
return prepareChannelForTask0()
598-
} else {
599-
return channel.eventLoop.flatSubmit {
600-
return prepareChannelForTask0()
601-
}
602-
}
603-
}.always { _ in
604-
setupComplete.succeed(())
605-
}.whenFailure { error in
606-
taskHandler.callOutToDelegateFireAndForget { task in
607-
delegate.didReceiveError(task: task, error)
545+
task.promise.futureResult.whenComplete { _ in
546+
deadlineSchedule?.cancel()
608547
}
609-
task.promise.fail(error)
610548
}
611549

550+
self.poolManager.execute(request: requestBag)
551+
612552
return task
613553
}
614554

@@ -815,7 +755,7 @@ public class HTTPClient {
815755

816756
enum State {
817757
case upAndRunning
818-
case shuttingDown
758+
case shuttingDown(requiresCleanClose: Bool, callback: (Error?) -> Void)
819759
case shutDown
820760
}
821761
}
@@ -882,6 +822,22 @@ extension HTTPClient.Configuration {
882822
}
883823
}
884824

825+
extension HTTPClient: HTTPConnectionPoolManagerDelegate {
826+
func httpConnectionPoolManagerDidShutdown(_: HTTPConnectionPool.Manager, unclean: Bool) {
827+
let (callback, error) = self.stateLock.withLock { () -> ((Error?) -> Void, Error?) in
828+
guard case .shuttingDown(let requiresClean, callback: let callback) = self.state else {
829+
preconditionFailure("Why did the pool manager shut down, if it was not instructed to")
830+
}
831+
832+
self.state = .shutDown
833+
let error: Error? = (requiresClean && unclean) ? HTTPClientError.uncleanShutdown : nil
834+
return (callback, error)
835+
}
836+
837+
callback(error)
838+
}
839+
}
840+
885841
/// Possible client errors.
886842
public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
887843
private enum Code: Equatable {
@@ -909,6 +865,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
909865
case incompatibleHeaders
910866
case connectTimeout
911867
case getConnectionFromPoolTimeout
868+
case deadlineExceeded
912869
}
913870

914871
private var code: Code
@@ -973,4 +930,6 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
973930
/// - A connection could not be created within the timout period.
974931
/// - Tasks are not processed fast enough on the existing connections, to process all waiters in time
975932
public static let getConnectionFromPoolTimeout = HTTPClientError(code: .getConnectionFromPoolTimeout)
933+
/// The request deadline was exceeded. The request was cancelled because of this.
934+
public static let deadlineExceeded = HTTPClientError(code: .deadlineExceeded)
976935
}

0 commit comments

Comments
 (0)