Skip to content

Commit bf033da

Browse files
Johannes Weissweissi
authored andcommitted
NIOSingletonsTransportServices: Use NIOTS in easy mode
1 parent fbc49d8 commit bf033da

File tree

5 files changed

+190
-3
lines changed

5 files changed

+190
-3
lines changed

Sources/NIOTransportServices/NIOTSEventLoop.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ internal class NIOTSEventLoop: QoSEventLoop {
5757
private let inQueueKey: DispatchSpecificKey<UUID>
5858
private let loopID: UUID
5959
private let defaultQoS: DispatchQoS
60+
private let canBeShutDownIndividually: Bool
6061

6162
/// All the channels registered to this event loop.
6263
///
@@ -96,12 +97,17 @@ internal class NIOTSEventLoop: QoSEventLoop {
9697
return DispatchQueue.getSpecific(key: self.inQueueKey) == self.loopID
9798
}
9899

99-
public init(qos: DispatchQoS) {
100+
public convenience init(qos: DispatchQoS) {
101+
self.init(qos: qos, canBeShutDownIndividually: true)
102+
}
103+
104+
internal init(qos: DispatchQoS, canBeShutDownIndividually: Bool) {
100105
self.loop = DispatchQueue(label: "nio.transportservices.eventloop.loop", qos: qos, autoreleaseFrequency: .workItem)
101106
self.taskQueue = DispatchQueue(label: "nio.transportservices.eventloop.taskqueue", target: self.loop)
102107
self.loopID = UUID()
103108
self.inQueueKey = DispatchSpecificKey()
104109
self.defaultQoS = qos
110+
self.canBeShutDownIndividually = canBeShutDownIndividually
105111
loop.setSpecific(key: inQueueKey, value: self.loopID)
106112
}
107113

@@ -158,6 +164,15 @@ internal class NIOTSEventLoop: QoSEventLoop {
158164
}
159165

160166
public func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
167+
guard self.canBeShutDownIndividually else {
168+
// The loops cannot be shut down by individually. They need to be shut down as a group and
169+
// `NIOTSEventLoopGroup` calls `closeGently` not this method.
170+
queue.async {
171+
callback(EventLoopError.unsupportedOperation)
172+
}
173+
return
174+
}
175+
161176
self.closeGently().map {
162177
queue.async { callback(nil) }
163178
}.whenFailure { error in

Sources/NIOTransportServices/NIOTSEventLoopGroup.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,32 @@ import Atomics
5050
public final class NIOTSEventLoopGroup: EventLoopGroup {
5151
private let index = ManagedAtomic(0)
5252
private let eventLoops: [NIOTSEventLoop]
53+
private let canBeShutDown: Bool
54+
55+
private init(eventLoops: [NIOTSEventLoop], canBeShutDown: Bool) {
56+
self.eventLoops = eventLoops
57+
self.canBeShutDown = canBeShutDown
58+
}
5359

5460
/// Construct a ``NIOTSEventLoopGroup`` with a specified number of loops and QoS.
5561
///
5662
/// - parameters:
5763
/// - loopCount: The number of independent loops to use. Defaults to `1`.
5864
/// - defaultQoS: The default QoS for tasks enqueued on this loop. Defaults to `.default`.
59-
public init(loopCount: Int = 1, defaultQoS: DispatchQoS = .default) {
65+
public convenience init(loopCount: Int = 1, defaultQoS: DispatchQoS = .default) {
66+
self.init(loopCount: loopCount, defaultQoS: defaultQoS, canBeShutDown: true)
67+
}
68+
69+
internal convenience init(loopCount: Int, defaultQoS: DispatchQoS, canBeShutDown: Bool) {
6070
precondition(loopCount > 0)
61-
self.eventLoops = (0..<loopCount).map { _ in NIOTSEventLoop(qos: defaultQoS) }
71+
let eventLoops = (0..<loopCount).map { _ in
72+
NIOTSEventLoop(qos: defaultQoS, canBeShutDownIndividually: false)
73+
}
74+
self.init(eventLoops: eventLoops, canBeShutDown: canBeShutDown)
75+
}
76+
77+
public static func _makePerpetualGroup(loopCount: Int, defaultQoS: DispatchQoS) -> Self {
78+
self.init(loopCount: loopCount, defaultQoS: defaultQoS, canBeShutDown: false)
6279
}
6380

6481
public func next() -> EventLoop {
@@ -67,6 +84,12 @@ public final class NIOTSEventLoopGroup: EventLoopGroup {
6784

6885
/// Shuts down all of the event loops, rendering them unable to perform further work.
6986
public func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
87+
guard self.canBeShutDown else {
88+
queue.async {
89+
callback(EventLoopError.unsupportedOperation)
90+
}
91+
return
92+
}
7093
let g = DispatchGroup()
7194
let q = DispatchQueue(label: "nio.transportservices.shutdowngracefullyqueue", target: queue)
7295
var error: Error? = nil
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#if canImport(Network)
16+
import NIOCore
17+
18+
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
19+
extension NIOTSEventLoopGroup {
20+
/// A globally shared, lazily initialized, singleton ``NIOTSEventLoopGroup``.
21+
///
22+
/// SwiftNIO allows and encourages the precise management of all operating system resources such as threads and file descriptors.
23+
/// Certain resources (such as the main `EventLoopGroup`) however are usually globally shared across the program. This means
24+
/// that many programs have to carry around an `EventLoopGroup` despite the fact they don't require the ability to fully return
25+
/// all the operating resources which would imply shutting down the `EventLoopGroup`. This type is the global handle for singleton
26+
/// resources that applications (and some libraries) can use to obtain never-shut-down singleton resources.
27+
///
28+
/// Programs and libraries that do not use these singletons will not incur extra resource usage, these resources are lazily initialized on
29+
/// first use.
30+
///
31+
/// The loop count of this group is determined by `NIOSingletons.groupLoopCountSuggestion`.
32+
///
33+
/// - note: Users who do not want any code to spawn global singleton resources may set
34+
/// `NIOSingletons.singletonsEnabledSuggestion` to `false` which will lead to a forced crash
35+
/// if any code attempts to use the global singletons.
36+
public static var singleton: NIOTSEventLoopGroup {
37+
return NIOSingletons.transportServicesEventLoopGroup
38+
}
39+
40+
}
41+
42+
extension NIOSingletons {
43+
/// A globally shared, lazily initialized, singleton ``NIOTSEventLoopGroup``.
44+
///
45+
/// SwiftNIO allows and encourages the precise management of all operating system resources such as threads and file descriptors.
46+
/// Certain resources (such as the main `EventLoopGroup`) however are usually globally shared across the program. This means
47+
/// that many programs have to carry around an `EventLoopGroup` despite the fact they don't require the ability to fully return
48+
/// all the operating resources which would imply shutting down the `EventLoopGroup`. This type is the global handle for singleton
49+
/// resources that applications (and some libraries) can use to obtain never-shut-down singleton resources.
50+
///
51+
/// Programs and libraries that do not use these singletons will not incur extra resource usage, these resources are lazily initialized on
52+
/// first use.
53+
///
54+
/// The loop count of this group is determined by `NIOSingletons.groupLoopCountSuggestion`.
55+
///
56+
/// - note: Users who do not want any code to spawn global singleton resources may set
57+
/// `NIOSingletons.singletonsEnabledSuggestion` to `false` which will lead to a forced crash
58+
/// if any code attempts to use the global singletons.
59+
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
60+
public static var transportServicesEventLoopGroup: NIOTSEventLoopGroup {
61+
return globalTransportServicesEventLoopGroup
62+
}
63+
}
64+
65+
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
66+
private let globalTransportServicesEventLoopGroup: NIOTSEventLoopGroup = {
67+
guard NIOSingletons.singletonsEnabledSuggestion else {
68+
fatalError("""
69+
Cannot create global singleton NIOThreadPool because the global singletons have been \
70+
disabled by setting `NIOSingletons.singletonsEnabledSuggestion = false`
71+
""")
72+
}
73+
74+
let group = NIOTSEventLoopGroup._makePerpetualGroup(loopCount: NIOSingletons.groupLoopCountSuggestion,
75+
defaultQoS: .default)
76+
_ = Unmanaged.passUnretained(group).retain() // Never gonna give you up, never gonna let you down.
77+
return group
78+
}()
79+
#endif

Tests/NIOTransportServicesTests/NIOTSEventLoopTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,5 +129,25 @@ class NIOTSEventLoopTest: XCTestCase {
129129
XCTAssertNil(weakELG)
130130
XCTAssertNil(weakEL)
131131
}
132+
133+
func testGroupCanBeShutDown() async throws {
134+
try await NIOTSEventLoopGroup(loopCount: 3).shutdownGracefully()
135+
}
136+
137+
func testIndividualLoopsCannotBeShutDownWhenPartOfGroup() async throws {
138+
let group = NIOTSEventLoopGroup(loopCount: 3)
139+
defer {
140+
try! group.syncShutdownGracefully()
141+
}
142+
143+
for loop in group.makeIterator() {
144+
do {
145+
try await loop.shutdownGracefully()
146+
XCTFail("this shouldn't work")
147+
} catch {
148+
XCTAssertEqual(.unsupportedOperation, error as? EventLoopError)
149+
}
150+
}
151+
}
132152
}
133153
#endif
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import XCTest
16+
import NIOTransportServices
17+
import NIOCore
18+
19+
final class NIOSingletonsTests: XCTestCase {
20+
func testNIOSingletonsTransportServicesEventLoopGroupWorks() async throws {
21+
let works = try await NIOSingletons.transportServicesEventLoopGroup.any().submit { "yes" }.get()
22+
XCTAssertEqual(works, "yes")
23+
}
24+
25+
func testNIOTSEventLoopGroupSingletonWorks() async throws {
26+
let works = try await NIOTSEventLoopGroup.singleton.any().submit { "yes" }.get()
27+
XCTAssertEqual(works, "yes")
28+
XCTAssert(NIOTSEventLoopGroup.singleton === NIOSingletons.transportServicesEventLoopGroup)
29+
}
30+
31+
func testSingletonGroupCannotBeShutDown() async throws {
32+
do {
33+
try await NIOTSEventLoopGroup.singleton.shutdownGracefully()
34+
XCTFail("shutdown worked, that's bad")
35+
} catch {
36+
XCTAssertEqual(EventLoopError.unsupportedOperation, error as? EventLoopError)
37+
}
38+
}
39+
40+
func testSingletonLoopThatArePartOfGroupCannotBeShutDown() async throws {
41+
for loop in NIOTSEventLoopGroup.singleton.makeIterator() {
42+
do {
43+
try await loop.shutdownGracefully()
44+
XCTFail("shutdown worked, that's bad")
45+
} catch {
46+
XCTAssertEqual(EventLoopError.unsupportedOperation, error as? EventLoopError)
47+
}
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)