@@ -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 {}
11131113extension HTTPClient . EventLoopGroupProvider : Sendable { }
11141114extension 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-
11191116extension HTTPClient . Configuration {
11201117 /// Timeout configuration.
11211118 public struct Timeout : Sendable {
0 commit comments