NIOSingletonsTransportServices: Use NIOTS in easy mode (#180)

Co-authored-by: Johannes Weiss <johannes@jweiss.io>
This commit is contained in:
Johannes Weiss 2023-08-09 11:29:24 +01:00 committed by GitHub
parent f73f69faf7
commit 39ece4ed45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 201 additions and 3 deletions

View File

@ -57,6 +57,7 @@ internal class NIOTSEventLoop: QoSEventLoop {
private let inQueueKey: DispatchSpecificKey<UUID> private let inQueueKey: DispatchSpecificKey<UUID>
private let loopID: UUID private let loopID: UUID
private let defaultQoS: DispatchQoS private let defaultQoS: DispatchQoS
private let canBeShutDownIndividually: Bool
/// All the channels registered to this event loop. /// All the channels registered to this event loop.
/// ///
@ -96,12 +97,17 @@ internal class NIOTSEventLoop: QoSEventLoop {
return DispatchQueue.getSpecific(key: self.inQueueKey) == self.loopID return DispatchQueue.getSpecific(key: self.inQueueKey) == self.loopID
} }
public init(qos: DispatchQoS) { public convenience init(qos: DispatchQoS) {
self.init(qos: qos, canBeShutDownIndividually: true)
}
internal init(qos: DispatchQoS, canBeShutDownIndividually: Bool) {
self.loop = DispatchQueue(label: "nio.transportservices.eventloop.loop", qos: qos, autoreleaseFrequency: .workItem) self.loop = DispatchQueue(label: "nio.transportservices.eventloop.loop", qos: qos, autoreleaseFrequency: .workItem)
self.taskQueue = DispatchQueue(label: "nio.transportservices.eventloop.taskqueue", target: self.loop) self.taskQueue = DispatchQueue(label: "nio.transportservices.eventloop.taskqueue", target: self.loop)
self.loopID = UUID() self.loopID = UUID()
self.inQueueKey = DispatchSpecificKey() self.inQueueKey = DispatchSpecificKey()
self.defaultQoS = qos self.defaultQoS = qos
self.canBeShutDownIndividually = canBeShutDownIndividually
loop.setSpecific(key: inQueueKey, value: self.loopID) loop.setSpecific(key: inQueueKey, value: self.loopID)
} }
@ -158,6 +164,15 @@ internal class NIOTSEventLoop: QoSEventLoop {
} }
public func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) { public func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
guard self.canBeShutDownIndividually else {
// The loops cannot be shut down by individually. They need to be shut down as a group and
// `NIOTSEventLoopGroup` calls `closeGently` not this method.
queue.async {
callback(EventLoopError.unsupportedOperation)
}
return
}
self.closeGently().map { self.closeGently().map {
queue.async { callback(nil) } queue.async { callback(nil) }
}.whenFailure { error in }.whenFailure { error in

View File

@ -50,15 +50,32 @@ import Atomics
public final class NIOTSEventLoopGroup: EventLoopGroup { public final class NIOTSEventLoopGroup: EventLoopGroup {
private let index = ManagedAtomic(0) private let index = ManagedAtomic(0)
private let eventLoops: [NIOTSEventLoop] private let eventLoops: [NIOTSEventLoop]
private let canBeShutDown: Bool
private init(eventLoops: [NIOTSEventLoop], canBeShutDown: Bool) {
self.eventLoops = eventLoops
self.canBeShutDown = canBeShutDown
}
/// Construct a ``NIOTSEventLoopGroup`` with a specified number of loops and QoS. /// Construct a ``NIOTSEventLoopGroup`` with a specified number of loops and QoS.
/// ///
/// - parameters: /// - parameters:
/// - loopCount: The number of independent loops to use. Defaults to `1`. /// - loopCount: The number of independent loops to use. Defaults to `1`.
/// - defaultQoS: The default QoS for tasks enqueued on this loop. Defaults to `.default`. /// - defaultQoS: The default QoS for tasks enqueued on this loop. Defaults to `.default`.
public init(loopCount: Int = 1, defaultQoS: DispatchQoS = .default) { public convenience init(loopCount: Int = 1, defaultQoS: DispatchQoS = .default) {
self.init(loopCount: loopCount, defaultQoS: defaultQoS, canBeShutDown: true)
}
internal convenience init(loopCount: Int, defaultQoS: DispatchQoS, canBeShutDown: Bool) {
precondition(loopCount > 0) precondition(loopCount > 0)
self.eventLoops = (0..<loopCount).map { _ in NIOTSEventLoop(qos: defaultQoS) } let eventLoops = (0..<loopCount).map { _ in
NIOTSEventLoop(qos: defaultQoS, canBeShutDownIndividually: false)
}
self.init(eventLoops: eventLoops, canBeShutDown: canBeShutDown)
}
public static func _makePerpetualGroup(loopCount: Int, defaultQoS: DispatchQoS) -> Self {
self.init(loopCount: loopCount, defaultQoS: defaultQoS, canBeShutDown: false)
} }
public func next() -> EventLoop { public func next() -> EventLoop {
@ -67,6 +84,12 @@ public final class NIOTSEventLoopGroup: EventLoopGroup {
/// Shuts down all of the event loops, rendering them unable to perform further work. /// Shuts down all of the event loops, rendering them unable to perform further work.
public func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) { public func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
guard self.canBeShutDown else {
queue.async {
callback(EventLoopError.unsupportedOperation)
}
return
}
let g = DispatchGroup() let g = DispatchGroup()
let q = DispatchQueue(label: "nio.transportservices.shutdowngracefullyqueue", target: queue) let q = DispatchQueue(label: "nio.transportservices.shutdowngracefullyqueue", target: queue)
var error: Error? = nil var error: Error? = nil

View File

@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#if canImport(Network)
import NIOCore
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
extension NIOTSEventLoopGroup {
/// A globally shared, lazily initialized, singleton ``NIOTSEventLoopGroup``.
///
/// SwiftNIO allows and encourages the precise management of all operating system resources such as threads and file descriptors.
/// Certain resources (such as the main `EventLoopGroup`) however are usually globally shared across the program. This means
/// that many programs have to carry around an `EventLoopGroup` despite the fact they don't require the ability to fully return
/// all the operating resources which would imply shutting down the `EventLoopGroup`. This type is the global handle for singleton
/// resources that applications (and some libraries) can use to obtain never-shut-down singleton resources.
///
/// Programs and libraries that do not use these singletons will not incur extra resource usage, these resources are lazily initialized on
/// first use.
///
/// The loop count of this group is determined by `NIOSingletons.groupLoopCountSuggestion`.
///
/// - note: Users who do not want any code to spawn global singleton resources may set
/// `NIOSingletons.singletonsEnabledSuggestion` to `false` which will lead to a forced crash
/// if any code attempts to use the global singletons.
public static var singleton: NIOTSEventLoopGroup {
return NIOSingletons.transportServicesEventLoopGroup
}
}
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
extension EventLoopGroup where Self == NIOTSEventLoopGroup {
/// A globally shared, lazily initialized, singleton ``NIOTSEventLoopGroup``.
///
/// This provides the same object as ``NIOTSEventLoopGroup/singleton``.
public static var singletonNIOTSEventLoopGroup: Self {
return NIOTSEventLoopGroup.singleton
}
}
extension NIOSingletons {
/// A globally shared, lazily initialized, singleton ``NIOTSEventLoopGroup``.
///
/// SwiftNIO allows and encourages the precise management of all operating system resources such as threads and file descriptors.
/// Certain resources (such as the main `EventLoopGroup`) however are usually globally shared across the program. This means
/// that many programs have to carry around an `EventLoopGroup` despite the fact they don't require the ability to fully return
/// all the operating resources which would imply shutting down the `EventLoopGroup`. This type is the global handle for singleton
/// resources that applications (and some libraries) can use to obtain never-shut-down singleton resources.
///
/// Programs and libraries that do not use these singletons will not incur extra resource usage, these resources are lazily initialized on
/// first use.
///
/// The loop count of this group is determined by `NIOSingletons.groupLoopCountSuggestion`.
///
/// - note: Users who do not want any code to spawn global singleton resources may set
/// `NIOSingletons.singletonsEnabledSuggestion` to `false` which will lead to a forced crash
/// if any code attempts to use the global singletons.
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
public static var transportServicesEventLoopGroup: NIOTSEventLoopGroup {
return globalTransportServicesEventLoopGroup
}
}
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
private let globalTransportServicesEventLoopGroup: NIOTSEventLoopGroup = {
guard NIOSingletons.singletonsEnabledSuggestion else {
fatalError("""
Cannot create global singleton NIOThreadPool because the global singletons have been \
disabled by setting `NIOSingletons.singletonsEnabledSuggestion = false`
""")
}
let group = NIOTSEventLoopGroup._makePerpetualGroup(loopCount: NIOSingletons.groupLoopCountSuggestion,
defaultQoS: .default)
_ = Unmanaged.passUnretained(group).retain() // Never gonna give you up, never gonna let you down.
return group
}()
#endif

View File

@ -129,5 +129,25 @@ class NIOTSEventLoopTest: XCTestCase {
XCTAssertNil(weakELG) XCTAssertNil(weakELG)
XCTAssertNil(weakEL) XCTAssertNil(weakEL)
} }
func testGroupCanBeShutDown() async throws {
try await NIOTSEventLoopGroup(loopCount: 3).shutdownGracefully()
}
func testIndividualLoopsCannotBeShutDownWhenPartOfGroup() async throws {
let group = NIOTSEventLoopGroup(loopCount: 3)
defer {
try! group.syncShutdownGracefully()
}
for loop in group.makeIterator() {
do {
try await loop.shutdownGracefully()
XCTFail("this shouldn't work")
} catch {
XCTAssertEqual(.unsupportedOperation, error as? EventLoopError)
}
}
}
} }
#endif #endif

View File

@ -0,0 +1,52 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#if canImport(Network)
import XCTest
import NIOTransportServices
import NIOCore
final class NIOSingletonsTests: XCTestCase {
func testNIOSingletonsTransportServicesEventLoopGroupWorks() async throws {
let works = try await NIOSingletons.transportServicesEventLoopGroup.any().submit { "yes" }.get()
XCTAssertEqual(works, "yes")
}
func testNIOTSEventLoopGroupSingletonWorks() async throws {
let works = try await NIOTSEventLoopGroup.singleton.any().submit { "yes" }.get()
XCTAssertEqual(works, "yes")
XCTAssert(NIOTSEventLoopGroup.singleton === NIOSingletons.transportServicesEventLoopGroup)
}
func testSingletonGroupCannotBeShutDown() async throws {
do {
try await NIOTSEventLoopGroup.singleton.shutdownGracefully()
XCTFail("shutdown worked, that's bad")
} catch {
XCTAssertEqual(EventLoopError.unsupportedOperation, error as? EventLoopError)
}
}
func testSingletonLoopThatArePartOfGroupCannotBeShutDown() async throws {
for loop in NIOTSEventLoopGroup.singleton.makeIterator() {
do {
try await loop.shutdownGracefully()
XCTFail("shutdown worked, that's bad")
} catch {
XCTAssertEqual(EventLoopError.unsupportedOperation, error as? EventLoopError)
}
}
}
}
#endif