Skip to content

Commit e6a533e

Browse files
Johannes Weissweissi
Johannes Weiss
authored andcommitted
use NIOSingletons EventLoops/NIOThreadPool instead of spawning new
1 parent e1c85a6 commit e6a533e

File tree

8 files changed

+87
-105
lines changed

8 files changed

+87
-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.18.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

+64-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

@@ -99,24 +98,27 @@ public class HTTPClient {
9998
/// - parameters:
10099
/// - eventLoopGroupProvider: Specify how `EventLoopGroup` will be created.
101100
/// - configuration: Client configuration.
102-
public required init(eventLoopGroupProvider: EventLoopGroupProvider,
103-
configuration: Configuration = Configuration(),
104-
backgroundActivityLogger: Logger) {
105-
self.eventLoopGroupProvider = eventLoopGroupProvider
106-
switch self.eventLoopGroupProvider {
101+
public convenience init(eventLoopGroupProvider: EventLoopGroupProvider,
102+
configuration: Configuration = Configuration(),
103+
backgroundActivityLogger: Logger) {
104+
let eventLoopGroup: any EventLoopGroup
105+
106+
switch eventLoopGroupProvider {
107107
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
108+
eventLoopGroup = group
109+
default: // handle `.createNew` without a deprecation warning
110+
eventLoopGroup = HTTPClient.defaultEventLoopGroup
119111
}
112+
113+
self.init(eventLoopGroup: eventLoopGroup,
114+
configuration: configuration,
115+
backgroundActivityLogger: backgroundActivityLogger)
116+
}
117+
118+
public required init(eventLoopGroup: any EventLoopGroup,
119+
configuration: Configuration = Configuration(),
120+
backgroundActivityLogger: Logger) {
121+
self.eventLoopGroup = eventLoopGroup
120122
self.configuration = configuration
121123
self.poolManager = HTTPConnectionPool.Manager(
122124
eventLoopGroup: self.eventLoopGroup,
@@ -214,55 +216,16 @@ public class HTTPClient {
214216
}
215217

216218
/// 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.
221219
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
220+
let promise = self.eventLoopGroup.any().makePromise(of: Void.self)
221+
self.shutdown(queue: .global()) { error in
222+
if let error = error {
223+
promise.fail(error)
224+
} else {
225+
promise.succeed(())
263226
}
264-
fileIOThreadPool.shutdownGracefully(queue: queue, callback)
265227
}
228+
return promise.futureResult
266229
}
267230

268231
private func shutdown(requiresCleanClose: Bool, queue: DispatchQueue, _ callback: @escaping ShutdownCallback) {
@@ -293,23 +256,20 @@ public class HTTPClient {
293256
let error: Error? = (requiresClean && unclean) ? HTTPClientError.uncleanShutdown : nil
294257
return (callback, error)
295258
}
296-
self.shutdownFileIOThreadPool(queue: queue) { ioThreadPoolError in
297-
self.shutdownEventLoop(queue: queue) { error in
298-
let reportedError = error ?? ioThreadPoolError ?? uncleanError
299-
callback(reportedError)
300-
}
259+
self.stateLock.withLock {
260+
self.state = .shutDown
261+
}
262+
queue.async {
263+
callback(uncleanError)
301264
}
302265
}
303266
}
304267
}
305268

306269
private func makeOrGetFileIOThreadPool() -> NIOThreadPool {
307270
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
271+
guard let fileIOThreadPool = self.fileIOThreadPool else {
272+
return NIOThreadPool.singleton
313273
}
314274
return fileIOThreadPool
315275
}
@@ -853,7 +813,12 @@ public class HTTPClient {
853813
public enum EventLoopGroupProvider {
854814
/// `EventLoopGroup` will be provided by the user. Owner of this group is responsible for its lifecycle.
855815
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.
816+
/// The original intention of this was that ``HTTPClient`` would create and own its own `EventLoopGroup` to
817+
/// facilitate use in programs that are not already using SwiftNIO.
818+
/// Since https://github.com/apple/swift-nio/pull/2471 however, SwiftNIO does provider global, shared singleton
819+
/// `EventLoopGroup`s that we can use. ``HTTPClient`` is no longer able to create & own its own
820+
/// `EventLoopGroup` which solves a whole host of issues around shutdown.
821+
@available(*, deprecated, renamed: "singleton", message: "Please use the singleton EventLoopGroup explicitly")
857822
case createNew
858823
}
859824

@@ -914,6 +879,30 @@ public class HTTPClient {
914879
}
915880
}
916881

882+
extension HTTPClient.EventLoopGroupProvider {
883+
public static var singleton: Self {
884+
return .shared(HTTPClient.defaultEventLoopGroup)
885+
}
886+
887+
}
888+
889+
extension HTTPClient {
890+
/// Returns the default `EventLoopGroup` singleton, automatically selecting the best for the platform.
891+
///
892+
/// This will select the concrete `EventLoopGroup` depending which platform this is running on.
893+
public static var defaultEventLoopGroup: EventLoopGroup {
894+
#if canImport(Network)
895+
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
896+
return NIOTSEventLoopGroup.singleton
897+
} else {
898+
return MultiThreadedEventLoopGroup.singleton
899+
}
900+
#else
901+
return MultiThreadedEventLoopGroup.singleton
902+
#endif
903+
}
904+
}
905+
917906
#if swift(>=5.7)
918907
extension HTTPClient.Configuration: Sendable {}
919908
#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)))

Tests/AsyncHTTPClientTests/HTTP2ClientTests.swift

+5-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import XCTest
2525

2626
class HTTP2ClientTests: XCTestCase {
2727
func makeDefaultHTTPClient(
28-
eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .createNew
28+
eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .singleton
2929
) -> HTTPClient {
3030
var config = HTTPClient.Configuration()
3131
config.tlsConfiguration = .clientDefault
@@ -40,7 +40,7 @@ class HTTP2ClientTests: XCTestCase {
4040

4141
func makeClientWithActiveHTTP2Connection<RequestHandler>(
4242
to bin: HTTPBin<RequestHandler>,
43-
eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .createNew
43+
eventLoopGroupProvider: HTTPClient.EventLoopGroupProvider = .singleton
4444
) -> HTTPClient {
4545
let client = self.makeDefaultHTTPClient(eventLoopGroupProvider: eventLoopGroupProvider)
4646
var response: HTTPClient.Response?
@@ -301,7 +301,7 @@ class HTTP2ClientTests: XCTestCase {
301301
config.httpVersion = .automatic
302302
config.timeout.read = .milliseconds(100)
303303
let client = HTTPClient(
304-
eventLoopGroupProvider: .createNew,
304+
eventLoopGroupProvider: .singleton,
305305
configuration: config,
306306
backgroundActivityLogger: Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
307307
)
@@ -322,7 +322,8 @@ class HTTP2ClientTests: XCTestCase {
322322
config.tlsConfiguration = tlsConfig
323323
config.httpVersion = .automatic
324324
let client = HTTPClient(
325-
eventLoopGroupProvider: .createNew,
325+
// TODO: Test fails if the provided ELG is a multi-threaded NIOTSEventLoopGroup (probably racy)
326+
eventLoopGroupProvider: .shared(bin.group),
326327
configuration: config,
327328
backgroundActivityLogger: Logger(label: "HTTPClient", factory: StreamLogHandler.standardOutput(label:))
328329
)

0 commit comments

Comments
 (0)