Skip to content

Commit eadd7e6

Browse files
author
Johannes Weiss
committed
use NIOSingletons EventLoops/NIOThreadPool instead of spawning new
1 parent df66c67 commit eadd7e6

File tree

8 files changed

+54
-92
lines changed

8 files changed

+54
-92
lines changed

Package.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ 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.57.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"),
@@ -38,6 +38,8 @@ let package = Package(
3838
.target(name: "CAsyncHTTPClient"),
3939
.product(name: "NIO", package: "swift-nio"),
4040
.product(name: "NIOCore", package: "swift-nio"),
41+
.product(name: "NIOSingletonsPosix", package: "swift-nio"),
42+
.product(name: "NIOSingletonsTransportServices", package: "swift-nio-transport-services"),
4143
.product(name: "NIOPosix", package: "swift-nio"),
4244
.product(name: "NIOHTTP1", package: "swift-nio"),
4345
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),

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: .globalSingleton)
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: .globalSingleton)
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: .globalSingleton)
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: .globalSingleton,
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: .globalSingleton,
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: .globalSingleton)
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: .globalSingleton)
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: .globalSingleton)
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: .globalSingleton)
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: .globalSingleton,
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

+30-63
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import NIOPosix
2323
import NIOSSL
2424
import NIOTLS
2525
import NIOTransportServices
26+
import NIOSingletonsPosix
27+
import NIOSingletonsTransportServices
2628

2729
extension Logger {
2830
private func requestInfo(_ request: HTTPClient.Request) -> Logger.Metadata.Value {
@@ -44,7 +46,7 @@ let globalRequestID = ManagedAtomic(0)
4446
/// Example:
4547
///
4648
/// ```swift
47-
/// let client = HTTPClient(eventLoopGroupProvider: .createNew)
49+
/// let client = HTTPClient(eventLoopGroupProvider: .globalSingleton)
4850
/// client.get(url: "https://swift.org", deadline: .now() + .seconds(1)).whenComplete { result in
4951
/// switch result {
5052
/// case .failure(let error):
@@ -106,15 +108,15 @@ public class HTTPClient {
106108
switch self.eventLoopGroupProvider {
107109
case .shared(let group):
108110
self.eventLoopGroup = group
109-
case .createNew:
111+
case .globalSingleton:
110112
#if canImport(Network)
111113
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
112-
self.eventLoopGroup = NIOTSEventLoopGroup()
114+
self.eventLoopGroup = NIOSingletons.multiThreadedTransportServicesEventLoopGroup
113115
} else {
114-
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
116+
self.eventLoopGroup = NIOSingletons.multiThreadedPosixEventLoopGroup
115117
}
116118
#else
117-
self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
119+
self.eventLoopGroup = NIOSingletons.multiThreadedPosixEventLoopGroup
118120
#endif
119121
}
120122
self.configuration = configuration
@@ -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 NIOSingletons.posixBlockingPool
313273
}
314274
return fileIOThreadPool
315275
}
@@ -853,8 +813,8 @@ 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.
857-
case createNew
816+
/// An `EventLoopGroup` from `NIOSingletons`.
817+
case globalSingleton
858818
}
859819

860820
/// Specifies how the library will treat the event loop passed by the user.
@@ -914,6 +874,13 @@ public class HTTPClient {
914874
}
915875
}
916876

877+
extension HTTPClient.EventLoopGroupProvider {
878+
@available(*, deprecated, renamed: "globalSingleton")
879+
public static var createNew: Self {
880+
return .globalSingleton
881+
}
882+
}
883+
917884
#if swift(>=5.7)
918885
extension HTTPClient.Configuration: Sendable {}
919886
#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 = .globalSingleton
2424
) -> HTTPClient {
2525
var config = HTTPClient.Configuration()
2626
config.tlsConfiguration = .clientDefault
@@ -500,7 +500,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
500500
let config = HTTPClient.Configuration()
501501
.enableFastFailureModeForTesting()
502502

503-
let localClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: config)
503+
let localClient = HTTPClient(eventLoopGroupProvider: .globalSingleton, configuration: config)
504504
defer { XCTAssertNoThrow(try localClient.syncShutdown()) }
505505
let request = HTTPClientRequest(url: "https://localhost:\(port)")
506506
await XCTAssertThrowsError(try await localClient.execute(request, deadline: .now() + .seconds(2))) { error in
@@ -566,7 +566,7 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
566566
// this is the actual configuration under test
567567
config.dnsOverride = ["example.com": "localhost"]
568568

569-
let localClient = HTTPClient(eventLoopGroupProvider: .createNew, configuration: config)
569+
let localClient = HTTPClient(eventLoopGroupProvider: .globalSingleton, configuration: config)
570570
defer { XCTAssertNoThrow(try localClient.syncShutdown()) }
571571
let request = HTTPClientRequest(url: "https://example.com:\(bin.port)/echohostheader")
572572
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 = .globalSingleton
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 = .globalSingleton
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: .globalSingleton,
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
)

Tests/AsyncHTTPClientTests/HTTPClientInformationalResponsesTests.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class HTTPClientReproTests: XCTestCase {
3737
}
3838
}
3939

40-
let client = HTTPClient(eventLoopGroupProvider: .createNew)
40+
let client = HTTPClient(eventLoopGroupProvider: .globalSingleton)
4141
defer { XCTAssertNoThrow(try client.syncShutdown()) }
4242

4343
let httpBin = HTTPBin(.http1_1(ssl: false, compress: false)) { _ in
@@ -91,7 +91,7 @@ final class HTTPClientReproTests: XCTestCase {
9191
}
9292
}
9393

94-
let client = HTTPClient(eventLoopGroupProvider: .createNew)
94+
let client = HTTPClient(eventLoopGroupProvider: .globalSingleton)
9595
defer { XCTAssertNoThrow(try client.syncShutdown()) }
9696

9797
let httpBin = HTTPBin(.http1_1(ssl: false, compress: false)) { _ in

Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class HTTPClientNIOTSTests: XCTestCase {
3838
}
3939

4040
func testCorrectEventLoopGroup() {
41-
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
41+
let httpClient = HTTPClient(eventLoopGroupProvider: .globalSingleton)
4242
defer {
4343
XCTAssertNoThrow(try httpClient.syncShutdown())
4444
}

0 commit comments

Comments
 (0)