Skip to content

Commit 606fce2

Browse files
author
Johannes Weiss
committed
use NIOSingletons EventLoops/NIOThreadPool instead of spawning new
1 parent e1c85a6 commit 606fce2

9 files changed

+123
-105
lines changed

Package.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ let package = Package(
2121
.library(name: "AsyncHTTPClient", targets: ["AsyncHTTPClient"]),
2222
],
2323
dependencies: [
24-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.50.0"),
24+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.58.0"),
2525
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.22.0"),
2626
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.19.0"),
2727
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.13.0"),
28-
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.11.4"),
28+
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.19.0"),
2929
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.4"),
3030
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
3131
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),

README.md

+10-14
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,10 @@ and `AsyncHTTPClient` dependency to your target:
2727

2828
The code snippet below illustrates how to make a simple GET request to a remote server.
2929

30-
Please note that the example will spawn a new `EventLoopGroup` which will _create fresh threads_ which is a very costly operation. In a real-world application that uses [SwiftNIO](https://github.com/apple/swift-nio) for other parts of your application (for example a web server), please prefer `eventLoopGroupProvider: .shared(myExistingEventLoopGroup)` to share the `EventLoopGroup` used by AsyncHTTPClient with other parts of your application.
31-
32-
If your application does not use SwiftNIO yet, it is acceptable to use `eventLoopGroupProvider: .createNew` but please make sure to share the returned `HTTPClient` instance throughout your whole application. Do not create a large number of `HTTPClient` instances with `eventLoopGroupProvider: .createNew`, this is very wasteful and might exhaust the resources of your program.
33-
3430
```swift
3531
import AsyncHTTPClient
3632

37-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
33+
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
3834

3935
/// MARK: - Using Swift Concurrency
4036
let request = HTTPClientRequest(url: "https://apple.com/")
@@ -78,7 +74,7 @@ The default HTTP Method is `GET`. In case you need to have more control over the
7874
```swift
7975
import AsyncHTTPClient
8076

81-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
77+
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
8278
do {
8379
var request = HTTPClientRequest(url: "https://apple.com/")
8480
request.method = .POST
@@ -103,7 +99,7 @@ try await httpClient.shutdown()
10399
```swift
104100
import AsyncHTTPClient
105101

106-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
102+
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
107103
defer {
108104
try? httpClient.syncShutdown()
109105
}
@@ -129,15 +125,15 @@ httpClient.execute(request: request).whenComplete { result in
129125
### Redirects following
130126
Enable follow-redirects behavior using the client configuration:
131127
```swift
132-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
128+
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton,
133129
configuration: HTTPClient.Configuration(followRedirects: true))
134130
```
135131

136132
### Timeouts
137133
Timeouts (connect and read) can also be set using the client configuration:
138134
```swift
139135
let timeout = HTTPClient.Configuration.Timeout(connect: .seconds(1), read: .seconds(1))
140-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew,
136+
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton,
141137
configuration: HTTPClient.Configuration(timeout: timeout))
142138
```
143139
or on a per-request basis:
@@ -151,7 +147,7 @@ The following example demonstrates how to count the number of bytes in a streami
151147

152148
#### Using Swift Concurrency
153149
```swift
154-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
150+
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
155151
do {
156152
let request = HTTPClientRequest(url: "https://apple.com/")
157153
let response = try await httpClient.execute(request, timeout: .seconds(30))
@@ -251,7 +247,7 @@ asynchronously, while reporting the download progress at the same time, like in
251247
example:
252248

253249
```swift
254-
let client = HTTPClient(eventLoopGroupProvider: .createNew)
250+
let client = HTTPClient(eventLoopGroupProvider: .singleton)
255251
let request = try HTTPClient.Request(
256252
url: "https://swift.org/builds/development/ubuntu1804/latest-build.yml"
257253
)
@@ -275,7 +271,7 @@ client.execute(request: request, delegate: delegate).futureResult
275271
### Unix Domain Socket Paths
276272
Connecting to servers bound to socket paths is easy:
277273
```swift
278-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
274+
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
279275
httpClient.execute(
280276
.GET,
281277
socketPath: "/tmp/myServer.socket",
@@ -285,7 +281,7 @@ httpClient.execute(
285281

286282
Connecting over TLS to a unix domain socket path is possible as well:
287283
```swift
288-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
284+
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton)
289285
httpClient.execute(
290286
.POST,
291287
secureSocketPath: "/tmp/myServer.socket",
@@ -312,7 +308,7 @@ The exclusive use of HTTP/1 is possible by setting `httpVersion` to `.http1Only`
312308
var configuration = HTTPClient.Configuration()
313309
configuration.httpVersion = .http1Only
314310
let client = HTTPClient(
315-
eventLoopGroupProvider: .createNew,
311+
eventLoopGroupProvider: .singleton,
316312
configuration: configuration
317313
)
318314
```

Sources/AsyncHTTPClient/Docs.docc/index.md

-4
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ and `AsyncHTTPClient` dependency to your target:
3131

3232
The code snippet below illustrates how to make a simple GET request to a remote server.
3333

34-
Please note that the example will spawn a new `EventLoopGroup` which will _create fresh threads_ which is a very costly operation. In a real-world application that uses [SwiftNIO](https://github.com/apple/swift-nio) for other parts of your application (for example a web server), please prefer `eventLoopGroupProvider: .shared(myExistingEventLoopGroup)` to share the `EventLoopGroup` used by AsyncHTTPClient with other parts of your application.
35-
36-
If your application does not use SwiftNIO yet, it is acceptable to use `eventLoopGroupProvider: .createNew` but please make sure to share the returned `HTTPClient` instance throughout your whole application. Do not create a large number of `HTTPClient` instances with `eventLoopGroupProvider: .createNew`, this is very wasteful and might exhaust the resources of your program.
37-
3834
```swift
3935
import AsyncHTTPClient
4036

Sources/AsyncHTTPClient/HTTPClient.swift

+82-75
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ let globalRequestID = ManagedAtomic(0)
4444
/// Example:
4545
///
4646
/// ```swift
47-
/// let client = HTTPClient(eventLoopGroupProvider: .createNew)
47+
/// let client = HTTPClient(eventLoopGroupProvider: .singleton)
4848
/// client.get(url: "https://swift.org", deadline: .now() + .seconds(1)).whenComplete { result in
4949
/// switch result {
5050
/// case .failure(let error):
@@ -69,7 +69,6 @@ public class HTTPClient {
6969
///
7070
/// All HTTP transactions will occur on loops owned by this group.
7171
public let eventLoopGroup: EventLoopGroup
72-
let eventLoopGroupProvider: EventLoopGroupProvider
7372
let configuration: Configuration
7473
let poolManager: HTTPConnectionPool.Manager
7574

@@ -94,29 +93,50 @@ public class HTTPClient {
9493
backgroundActivityLogger: HTTPClient.loggingDisabled)
9594
}
9695

96+
/// Create an ``HTTPClient`` with specified `EventLoopGroup` and configuration.
97+
///
98+
/// - parameters:
99+
/// - eventLoopGroupProvider: Specify how `EventLoopGroup` will be created.
100+
/// - configuration: Client configuration.
101+
public convenience init(eventLoopGroup: EventLoopGroup = HTTPClient.defaultEventLoopGroup,
102+
configuration: Configuration = Configuration()) {
103+
self.init(eventLoopGroupProvider: .shared(eventLoopGroup),
104+
configuration: configuration,
105+
backgroundActivityLogger: HTTPClient.loggingDisabled)
106+
}
107+
97108
/// Create an ``HTTPClient`` with specified `EventLoopGroup` provider and configuration.
98109
///
99110
/// - parameters:
100111
/// - eventLoopGroupProvider: Specify how `EventLoopGroup` will be created.
101112
/// - configuration: Client configuration.
102-
public required init(eventLoopGroupProvider: EventLoopGroupProvider,
103-
configuration: Configuration = Configuration(),
104-
backgroundActivityLogger: Logger) {
105-
self.eventLoopGroupProvider = eventLoopGroupProvider
106-
switch self.eventLoopGroupProvider {
113+
public convenience init(eventLoopGroupProvider: EventLoopGroupProvider,
114+
configuration: Configuration = Configuration(),
115+
backgroundActivityLogger: Logger) {
116+
let eventLoopGroup: any EventLoopGroup
117+
118+
switch eventLoopGroupProvider {
107119
case .shared(let group):
108-
self.eventLoopGroup = group
109-
case .createNew:
110-
#if canImport(Network)
111-
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
112-
self.eventLoopGroup = NIOTSEventLoopGroup()
113-
} else {
114-
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
115-
}
116-
#else
117-
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
118-
#endif
120+
eventLoopGroup = group
121+
default: // handle `.createNew` without a deprecation warning
122+
eventLoopGroup = HTTPClient.defaultEventLoopGroup
119123
}
124+
125+
self.init(eventLoopGroup: eventLoopGroup,
126+
configuration: configuration,
127+
backgroundActivityLogger: backgroundActivityLogger)
128+
}
129+
130+
/// Create an ``HTTPClient`` with specified `EventLoopGroup` and configuration.
131+
///
132+
/// - parameters:
133+
/// - eventLoopGroup: The `EventLoopGroup` that the ``HTTPClient`` will use.
134+
/// - configuration: Client configuration.
135+
/// - backgroundActivityLogger: The `Logger` that will be used to log background any activity that's not associated with a request.
136+
public required init(eventLoopGroup: any EventLoopGroup,
137+
configuration: Configuration = Configuration(),
138+
backgroundActivityLogger: Logger) {
139+
self.eventLoopGroup = eventLoopGroup
120140
self.configuration = configuration
121141
self.poolManager = HTTPConnectionPool.Manager(
122142
eventLoopGroup: self.eventLoopGroup,
@@ -214,55 +234,16 @@ public class HTTPClient {
214234
}
215235

216236
/// Shuts down the ``HTTPClient`` and releases its resources.
217-
///
218-
/// - note: You cannot use this method if you sharted the ``HTTPClient`` with
219-
/// `init(eventLoopGroupProvider: .createNew)` because that will shut down the `EventLoopGroup` the
220-
/// returned future would run in.
221237
public func shutdown() -> EventLoopFuture<Void> {
222-
switch self.eventLoopGroupProvider {
223-
case .shared(let group):
224-
let promise = group.any().makePromise(of: Void.self)
225-
self.shutdown(queue: .global()) { error in
226-
if let error = error {
227-
promise.fail(error)
228-
} else {
229-
promise.succeed(())
230-
}
231-
}
232-
return promise.futureResult
233-
case .createNew:
234-
preconditionFailure("Cannot use the shutdown() method which returns a future when owning the EventLoopGroup. Please use the one of the other shutdown methods.")
235-
}
236-
}
237-
238-
private func shutdownEventLoop(queue: DispatchQueue, _ callback: @escaping ShutdownCallback) {
239-
self.stateLock.withLock {
240-
switch self.eventLoopGroupProvider {
241-
case .shared:
242-
self.state = .shutDown
243-
queue.async {
244-
callback(nil)
245-
}
246-
case .createNew:
247-
switch self.state {
248-
case .shuttingDown:
249-
self.state = .shutDown
250-
self.eventLoopGroup.shutdownGracefully(queue: queue, callback)
251-
case .shutDown, .upAndRunning:
252-
assertionFailure("The only valid state at this point is \(String(describing: State.shuttingDown))")
253-
}
254-
}
255-
}
256-
}
257-
258-
private func shutdownFileIOThreadPool(queue: DispatchQueue, _ callback: @escaping ShutdownCallback) {
259-
self.fileIOThreadPoolLock.withLock {
260-
guard let fileIOThreadPool = fileIOThreadPool else {
261-
callback(nil)
262-
return
238+
let promise = self.eventLoopGroup.any().makePromise(of: Void.self)
239+
self.shutdown(queue: .global()) { error in
240+
if let error = error {
241+
promise.fail(error)
242+
} else {
243+
promise.succeed(())
263244
}
264-
fileIOThreadPool.shutdownGracefully(queue: queue, callback)
265245
}
246+
return promise.futureResult
266247
}
267248

268249
private func shutdown(requiresCleanClose: Bool, queue: DispatchQueue, _ callback: @escaping ShutdownCallback) {
@@ -293,23 +274,20 @@ public class HTTPClient {
293274
let error: Error? = (requiresClean && unclean) ? HTTPClientError.uncleanShutdown : nil
294275
return (callback, error)
295276
}
296-
self.shutdownFileIOThreadPool(queue: queue) { ioThreadPoolError in
297-
self.shutdownEventLoop(queue: queue) { error in
298-
let reportedError = error ?? ioThreadPoolError ?? uncleanError
299-
callback(reportedError)
300-
}
277+
self.stateLock.withLock {
278+
self.state = .shutDown
279+
}
280+
queue.async {
281+
callback(uncleanError)
301282
}
302283
}
303284
}
304285
}
305286

306287
private func makeOrGetFileIOThreadPool() -> NIOThreadPool {
307288
self.fileIOThreadPoolLock.withLock {
308-
guard let fileIOThreadPool = fileIOThreadPool else {
309-
let fileIOThreadPool = NIOThreadPool(numberOfThreads: System.coreCount)
310-
fileIOThreadPool.start()
311-
self.fileIOThreadPool = fileIOThreadPool
312-
return fileIOThreadPool
289+
guard let fileIOThreadPool = self.fileIOThreadPool else {
290+
return NIOThreadPool.singleton
313291
}
314292
return fileIOThreadPool
315293
}
@@ -853,7 +831,12 @@ public class HTTPClient {
853831
public enum EventLoopGroupProvider {
854832
/// `EventLoopGroup` will be provided by the user. Owner of this group is responsible for its lifecycle.
855833
case shared(EventLoopGroup)
856-
/// `EventLoopGroup` will be created by the client. When ``HTTPClient/syncShutdown()`` is called, the created `EventLoopGroup` will be shut down as well.
834+
/// The original intention of this was that ``HTTPClient`` would create and own its own `EventLoopGroup` to
835+
/// facilitate use in programs that are not already using SwiftNIO.
836+
/// Since https://github.com/apple/swift-nio/pull/2471 however, SwiftNIO does provider global, shared singleton
837+
/// `EventLoopGroup`s that we can use. ``HTTPClient`` is no longer able to create & own its own
838+
/// `EventLoopGroup` which solves a whole host of issues around shutdown.
839+
@available(*, deprecated, renamed: "singleton", message: "Please use the singleton EventLoopGroup explicitly")
857840
case createNew
858841
}
859842

@@ -914,6 +897,30 @@ public class HTTPClient {
914897
}
915898
}
916899

900+
extension HTTPClient.EventLoopGroupProvider {
901+
public static var singleton: Self {
902+
return .shared(HTTPClient.defaultEventLoopGroup)
903+
}
904+
905+
}
906+
907+
extension HTTPClient {
908+
/// Returns the default `EventLoopGroup` singleton, automatically selecting the best for the platform.
909+
///
910+
/// This will select the concrete `EventLoopGroup` depending which platform this is running on.
911+
public static var defaultEventLoopGroup: EventLoopGroup {
912+
#if canImport(Network)
913+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
914+
return NIOTSEventLoopGroup.singleton
915+
} else {
916+
return MultiThreadedEventLoopGroup.singleton
917+
}
918+
#else
919+
return MultiThreadedEventLoopGroup.singleton
920+
#endif
921+
}
922+
}
923+
917924
#if swift(>=5.7)
918925
extension HTTPClient.Configuration: Sendable {}
919926
#endif

Tests/AsyncHTTPClientTests/AsyncAwaitEndToEndTests.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import NIOSSL
2020
import XCTest
2121

2222
private func makeDefaultHTTPClient(
23-
eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .createNew
23+
eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .singleton
2424
) -> HTTPClient {
2525
var config = HTTPClient.Configuration()
2626
config.tlsConfiguration = .clientDefault
@@ -504,7 +504,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
504504
let config = HTTPClient.Configuration()
505505
.enableFastFailureModeForTesting()
506506

507-
let localClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: config)
507+
let localClient = HTTPClient(eventLoopGroupProvider: .singleton, configuration: config)
508508
defer { XCTAssertNoThrow(try localClient.syncShutdown()) }
509509
let request = HTTPClientRequest(url: "https://localhost:\(port)")
510510
await XCTAssertThrowsError(try await localClient.execute(request, deadline: .now() + .seconds(2))) { error in
@@ -570,7 +570,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
570570
// this is the actual configuration under test
571571
config.dnsOverride = ["example.com": "localhost"]
572572

573-
let localClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: config)
573+
let localClient = HTTPClient(eventLoopGroupProvider: .singleton, configuration: config)
574574
defer { XCTAssertNoThrow(try localClient.syncShutdown()) }
575575
let request = HTTPClientRequest(url: "https://example.com:\(bin.port)/echohostheader")
576576
let response = await XCTAssertNoThrowWithResult(try await localClient.execute(request, deadline: .now() + .seconds(2)))

0 commit comments

Comments
 (0)