Skip to content

Commit aa19565

Browse files
committed
make HTTPClient Sendable
1 parent 2edac1d commit aa19565

File tree

2 files changed

+60
-61
lines changed

2 files changed

+60
-61
lines changed

Sources/AsyncHTTPClient/ConnectionPool/HTTPConnectionPool+Manager.swift

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,26 @@ import NIOCore
1919
import NIOHTTP1
2020

2121
extension HTTPConnectionPool {
22-
final class Manager {
22+
final class Manager: Sendable {
2323
private typealias Key = ConnectionPool.Key
2424

25-
private enum State {
25+
private enum RunState: Sendable {
2626
case active
2727
case shuttingDown(promise: EventLoopPromise<Bool>?, unclean: Bool)
2828
case shutDown
2929
}
3030

31+
private struct State: Sendable {
32+
var runState: RunState = .active
33+
var pools: [Key: HTTPConnectionPool] = [:]
34+
}
35+
3136
private let eventLoopGroup: EventLoopGroup
3237
private let configuration: HTTPClient.Configuration
3338
private let connectionIDGenerator = Connection.ID.globalGenerator
3439
private let logger: Logger
3540

36-
private var state: State = .active
37-
private var _pools: [Key: HTTPConnectionPool] = [:]
38-
private let lock = NIOLock()
39-
41+
private let state: NIOLockedValueBox<State> = NIOLockedValueBox(State())
4042
private let sslContextCache = SSLContextCache()
4143

4244
init(
@@ -51,10 +53,10 @@ extension HTTPConnectionPool {
5153

5254
func executeRequest(_ request: HTTPSchedulableRequest) {
5355
let poolKey = request.poolKey
54-
let poolResult = self.lock.withLock { () -> Result<HTTPConnectionPool, HTTPClientError> in
55-
switch self.state {
56+
let poolResult = self.state.withLockedValue { state -> Result<HTTPConnectionPool, HTTPClientError> in
57+
switch state.runState {
5658
case .active:
57-
if let pool = self._pools[poolKey] {
59+
if let pool = state.pools[poolKey] {
5860
return .success(pool)
5961
}
6062

@@ -68,7 +70,7 @@ extension HTTPConnectionPool {
6870
idGenerator: self.connectionIDGenerator,
6971
backgroundActivityLogger: self.logger
7072
)
71-
self._pools[poolKey] = pool
73+
state.pools[poolKey] = pool
7274
return .success(pool)
7375

7476
case .shuttingDown, .shutDown:
@@ -95,17 +97,17 @@ extension HTTPConnectionPool {
9597
case shutdown([Key: HTTPConnectionPool])
9698
}
9799

98-
let action = self.lock.withLock { () -> ShutdownAction in
99-
switch self.state {
100+
let action = self.state.withLockedValue { state -> ShutdownAction in
101+
switch state.runState {
100102
case .active:
101103
// If there aren't any pools, we can mark the pool as shut down right away.
102-
if self._pools.isEmpty {
103-
self.state = .shutDown
104+
if state.pools.isEmpty {
105+
state.runState = .shutDown
104106
return .done(promise)
105107
} else {
106108
// this promise will be succeeded once all connection pools are shutdown
107-
self.state = .shuttingDown(promise: promise, unclean: false)
108-
return .shutdown(self._pools)
109+
state.runState = .shuttingDown(promise: promise, unclean: false)
110+
return .shutdown(state.pools)
109111
}
110112

111113
case .shuttingDown, .shutDown:
@@ -135,23 +137,23 @@ extension HTTPConnectionPool.Manager: HTTPConnectionPoolDelegate {
135137
case wait
136138
}
137139

138-
let closeAction = self.lock.withLock { () -> CloseAction in
139-
switch self.state {
140+
let closeAction = self.state.withLockedValue { state -> CloseAction in
141+
switch state.runState {
140142
case .active, .shutDown:
141143
preconditionFailure("Why are pools shutting down, if the manager did not give a signal")
142144

143145
case .shuttingDown(let promise, let soFarUnclean):
144-
guard self._pools.removeValue(forKey: pool.key) === pool else {
146+
guard state.pools.removeValue(forKey: pool.key) === pool else {
145147
preconditionFailure(
146148
"Expected that the pool was created by this manager and is known for this reason."
147149
)
148150
}
149151

150-
if self._pools.isEmpty {
151-
self.state = .shutDown
152+
if state.pools.isEmpty {
153+
state.runState = .shutDown
152154
return .close(promise, unclean: soFarUnclean || unclean)
153155
} else {
154-
self.state = .shuttingDown(promise: promise, unclean: soFarUnclean || unclean)
156+
state.runState = .shuttingDown(promise: promise, unclean: soFarUnclean || unclean)
155157
return .wait
156158
}
157159
}

Sources/AsyncHTTPClient/HTTPClient.swift

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ let globalRequestID = ManagedAtomic(0)
5757
/// }
5858
/// }
5959
/// ```
60-
public class HTTPClient {
60+
public final class HTTPClient: Sendable {
6161
/// The `EventLoopGroup` in use by this ``HTTPClient``.
6262
///
6363
/// All HTTP transactions will occur on loops owned by this group.
@@ -66,11 +66,9 @@ public class HTTPClient {
6666
let poolManager: HTTPConnectionPool.Manager
6767

6868
/// Shared thread pool used for file IO. It is lazily created on first access of ``Task/fileIOThreadPool``.
69-
private var fileIOThreadPool: NIOThreadPool?
70-
private let fileIOThreadPoolLock = NIOLock()
69+
private let fileIOThreadPool: NIOLockedValueBox<NIOThreadPool?>
7170

72-
private var state: State
73-
private let stateLock = NIOLock()
71+
private let state: NIOLockedValueBox<State>
7472
private let canBeShutDown: Bool
7573

7674
static let loggingDisabled = Logger(label: "AHC-do-not-log", factory: { _ in SwiftLogNoOpLogHandler() })
@@ -167,29 +165,32 @@ public class HTTPClient {
167165
configuration: self.configuration,
168166
backgroundActivityLogger: backgroundActivityLogger
169167
)
170-
self.state = .upAndRunning
168+
self.state = NIOLockedValueBox(.upAndRunning)
169+
self.fileIOThreadPool = NIOLockedValueBox(nil)
171170
}
172171

173172
deinit {
174173
debugOnly {
175174
// We want to crash only in debug mode.
176-
switch self.state {
177-
case .shutDown:
178-
break
179-
case .shuttingDown:
180-
preconditionFailure(
181-
"""
182-
This state should be totally unreachable. While the HTTPClient is shutting down a \
183-
reference cycle should exist, that prevents it from deinit.
184-
"""
185-
)
186-
case .upAndRunning:
187-
preconditionFailure(
188-
"""
189-
Client not shut down before the deinit. Please call client.shutdown() when no \
190-
longer needed. Otherwise memory will leak.
191-
"""
192-
)
175+
self.state.withLockedValue { state in
176+
switch state {
177+
case .shutDown:
178+
break
179+
case .shuttingDown:
180+
preconditionFailure(
181+
"""
182+
This state should be totally unreachable. While the HTTPClient is shutting down a \
183+
reference cycle should exist, that prevents it from deinit.
184+
"""
185+
)
186+
case .upAndRunning:
187+
preconditionFailure(
188+
"""
189+
Client not shut down before the deinit. Please call client.shutdown() when no \
190+
longer needed. Otherwise memory will leak.
191+
"""
192+
)
193+
}
193194
}
194195
}
195196
}
@@ -302,11 +303,11 @@ public class HTTPClient {
302303
return
303304
}
304305
do {
305-
try self.stateLock.withLock {
306-
guard case .upAndRunning = self.state else {
306+
try self.state.withLockedValue { state in
307+
guard case .upAndRunning = state else {
307308
throw HTTPClientError.alreadyShutdown
308309
}
309-
self.state = .shuttingDown(requiresCleanClose: requiresCleanClose, callback: callback)
310+
state = .shuttingDown(requiresCleanClose: requiresCleanClose, callback: callback)
310311
}
311312
} catch {
312313
callback(error)
@@ -320,17 +321,16 @@ public class HTTPClient {
320321
case .failure:
321322
preconditionFailure("Shutting down the connection pool must not fail, ever.")
322323
case .success(let unclean):
323-
let (callback, uncleanError) = self.stateLock.withLock { () -> (ShutdownCallback, Error?) in
324-
guard case .shuttingDown(let requiresClean, callback: let callback) = self.state else {
324+
let (callback, uncleanError) = self.state.withLockedValue {
325+
(state: inout HTTPClient.State) -> (ShutdownCallback, Error?) in
326+
guard case .shuttingDown(let requiresClean, callback: let callback) = state else {
325327
preconditionFailure("Why did the pool manager shut down, if it was not instructed to")
326328
}
327329

328330
let error: Error? = (requiresClean && unclean) ? HTTPClientError.uncleanShutdown : nil
331+
state = .shutDown
329332
return (callback, error)
330333
}
331-
self.stateLock.withLock {
332-
self.state = .shutDown
333-
}
334334
queue.async {
335335
callback(uncleanError)
336336
}
@@ -340,11 +340,11 @@ public class HTTPClient {
340340

341341
@Sendable
342342
private func makeOrGetFileIOThreadPool() -> NIOThreadPool {
343-
self.fileIOThreadPoolLock.withLock {
344-
guard let fileIOThreadPool = self.fileIOThreadPool else {
343+
self.fileIOThreadPool.withLockedValue { pool in
344+
guard let pool else {
345345
return NIOThreadPool.singleton
346346
}
347-
return fileIOThreadPool
347+
return pool
348348
}
349349
}
350350

@@ -734,8 +734,8 @@ public class HTTPClient {
734734
]
735735
)
736736

737-
let failedTask: Task<Delegate.Response>? = self.stateLock.withLock {
738-
switch self.state {
737+
let failedTask: Task<Delegate.Response>? = self.state.withLockedValue { state in
738+
switch state {
739739
case .upAndRunning:
740740
return nil
741741
case .shuttingDown, .shutDown:
@@ -1113,9 +1113,6 @@ extension HTTPClient.Configuration: Sendable {}
11131113
extension HTTPClient.EventLoopGroupProvider: Sendable {}
11141114
extension HTTPClient.EventLoopPreference: Sendable {}
11151115

1116-
// HTTPClient is thread-safe because its shared mutable state is protected through a lock
1117-
extension HTTPClient: @unchecked Sendable {}
1118-
11191116
extension HTTPClient.Configuration {
11201117
/// Timeout configuration.
11211118
public struct Timeout: Sendable {

0 commit comments

Comments
 (0)