@@ -57,7 +57,7 @@ let globalRequestID = ManagedAtomic(0)
57
57
/// }
58
58
/// }
59
59
/// ```
60
- public class HTTPClient {
60
+ public final class HTTPClient : Sendable {
61
61
/// The `EventLoopGroup` in use by this ``HTTPClient``.
62
62
///
63
63
/// All HTTP transactions will occur on loops owned by this group.
@@ -66,11 +66,9 @@ public class HTTPClient {
66
66
let poolManager : HTTPConnectionPool . Manager
67
67
68
68
/// 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 ? >
71
70
72
- private var state : State
73
- private let stateLock = NIOLock ( )
71
+ private let state : NIOLockedValueBox < State >
74
72
private let canBeShutDown : Bool
75
73
76
74
static let loggingDisabled = Logger ( label: " AHC-do-not-log " , factory: { _ in SwiftLogNoOpLogHandler ( ) } )
@@ -167,29 +165,32 @@ public class HTTPClient {
167
165
configuration: self . configuration,
168
166
backgroundActivityLogger: backgroundActivityLogger
169
167
)
170
- self . state = . upAndRunning
168
+ self . state = NIOLockedValueBox ( . upAndRunning)
169
+ self . fileIOThreadPool = NIOLockedValueBox ( nil )
171
170
}
172
171
173
172
deinit {
174
173
debugOnly {
175
174
// 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
+ }
193
194
}
194
195
}
195
196
}
@@ -302,11 +303,11 @@ public class HTTPClient {
302
303
return
303
304
}
304
305
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 {
307
308
throw HTTPClientError . alreadyShutdown
308
309
}
309
- self . state = . shuttingDown( requiresCleanClose: requiresCleanClose, callback: callback)
310
+ state = . shuttingDown( requiresCleanClose: requiresCleanClose, callback: callback)
310
311
}
311
312
} catch {
312
313
callback ( error)
@@ -320,17 +321,16 @@ public class HTTPClient {
320
321
case . failure:
321
322
preconditionFailure ( " Shutting down the connection pool must not fail, ever. " )
322
323
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 {
325
327
preconditionFailure ( " Why did the pool manager shut down, if it was not instructed to " )
326
328
}
327
329
328
330
let error : Error ? = ( requiresClean && unclean) ? HTTPClientError . uncleanShutdown : nil
331
+ state = . shutDown
329
332
return ( callback, error)
330
333
}
331
- self . stateLock. withLock {
332
- self . state = . shutDown
333
- }
334
334
queue. async {
335
335
callback ( uncleanError)
336
336
}
@@ -340,11 +340,11 @@ public class HTTPClient {
340
340
341
341
@Sendable
342
342
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 {
345
345
return NIOThreadPool . singleton
346
346
}
347
- return fileIOThreadPool
347
+ return pool
348
348
}
349
349
}
350
350
@@ -734,8 +734,8 @@ public class HTTPClient {
734
734
]
735
735
)
736
736
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 {
739
739
case . upAndRunning:
740
740
return nil
741
741
case . shuttingDown, . shutDown:
@@ -1113,9 +1113,6 @@ extension HTTPClient.Configuration: Sendable {}
1113
1113
extension HTTPClient . EventLoopGroupProvider : Sendable { }
1114
1114
extension HTTPClient . EventLoopPreference : Sendable { }
1115
1115
1116
- // HTTPClient is thread-safe because its shared mutable state is protected through a lock
1117
- extension HTTPClient : @unchecked Sendable { }
1118
-
1119
1116
extension HTTPClient . Configuration {
1120
1117
/// Timeout configuration.
1121
1118
public struct Timeout : Sendable {
0 commit comments