NIOSingletonsTransportServices: Use NIOTS in easy mode (#180)
Co-authored-by: Johannes Weiss <johannes@jweiss.io>
This commit is contained in:
parent
f73f69faf7
commit
39ece4ed45
|
|
@ -57,6 +57,7 @@ internal class NIOTSEventLoop: QoSEventLoop {
|
|||
private let inQueueKey: DispatchSpecificKey<UUID>
|
||||
private let loopID: UUID
|
||||
private let defaultQoS: DispatchQoS
|
||||
private let canBeShutDownIndividually: Bool
|
||||
|
||||
/// All the channels registered to this event loop.
|
||||
///
|
||||
|
|
@ -96,12 +97,17 @@ internal class NIOTSEventLoop: QoSEventLoop {
|
|||
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.taskQueue = DispatchQueue(label: "nio.transportservices.eventloop.taskqueue", target: self.loop)
|
||||
self.loopID = UUID()
|
||||
self.inQueueKey = DispatchSpecificKey()
|
||||
self.defaultQoS = qos
|
||||
self.canBeShutDownIndividually = canBeShutDownIndividually
|
||||
loop.setSpecific(key: inQueueKey, value: self.loopID)
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +164,15 @@ internal class NIOTSEventLoop: QoSEventLoop {
|
|||
}
|
||||
|
||||
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 {
|
||||
queue.async { callback(nil) }
|
||||
}.whenFailure { error in
|
||||
|
|
|
|||
|
|
@ -50,15 +50,32 @@ import Atomics
|
|||
public final class NIOTSEventLoopGroup: EventLoopGroup {
|
||||
private let index = ManagedAtomic(0)
|
||||
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.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - loopCount: The number of independent loops to use. Defaults to `1`.
|
||||
/// - 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)
|
||||
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 {
|
||||
|
|
@ -67,6 +84,12 @@ public final class NIOTSEventLoopGroup: EventLoopGroup {
|
|||
|
||||
/// Shuts down all of the event loops, rendering them unable to perform further work.
|
||||
public func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
|
||||
guard self.canBeShutDown else {
|
||||
queue.async {
|
||||
callback(EventLoopError.unsupportedOperation)
|
||||
}
|
||||
return
|
||||
}
|
||||
let g = DispatchGroup()
|
||||
let q = DispatchQueue(label: "nio.transportservices.shutdowngracefullyqueue", target: queue)
|
||||
var error: Error? = nil
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -129,5 +129,25 @@ class NIOTSEventLoopTest: XCTestCase {
|
|||
XCTAssertNil(weakELG)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue