From 39ece4ed456a7d61249a080173ff216833ca5466 Mon Sep 17 00:00:00 2001 From: Johannes Weiss Date: Wed, 9 Aug 2023 11:29:24 +0100 Subject: [PATCH] NIOSingletonsTransportServices: Use NIOTS in easy mode (#180) Co-authored-by: Johannes Weiss --- .../NIOTransportServices/NIOTSEventLoop.swift | 17 +++- .../NIOTSEventLoopGroup.swift | 27 +++++- .../NIOTSSingletons.swift | 88 +++++++++++++++++++ .../NIOTSEventLoopTests.swift | 20 +++++ .../NIOTSSingletonTests.swift | 52 +++++++++++ 5 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 Sources/NIOTransportServices/NIOTSSingletons.swift create mode 100644 Tests/NIOTransportServicesTests/NIOTSSingletonTests.swift diff --git a/Sources/NIOTransportServices/NIOTSEventLoop.swift b/Sources/NIOTransportServices/NIOTSEventLoop.swift index b0bb6cd..ff0d007 100644 --- a/Sources/NIOTransportServices/NIOTSEventLoop.swift +++ b/Sources/NIOTransportServices/NIOTSEventLoop.swift @@ -57,6 +57,7 @@ internal class NIOTSEventLoop: QoSEventLoop { private let inQueueKey: DispatchSpecificKey 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 diff --git a/Sources/NIOTransportServices/NIOTSEventLoopGroup.swift b/Sources/NIOTransportServices/NIOTSEventLoopGroup.swift index b27fc0d..9037652 100644 --- a/Sources/NIOTransportServices/NIOTSEventLoopGroup.swift +++ b/Sources/NIOTransportServices/NIOTSEventLoopGroup.swift @@ -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.. 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 diff --git a/Sources/NIOTransportServices/NIOTSSingletons.swift b/Sources/NIOTransportServices/NIOTSSingletons.swift new file mode 100644 index 0000000..fb399dc --- /dev/null +++ b/Sources/NIOTransportServices/NIOTSSingletons.swift @@ -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 diff --git a/Tests/NIOTransportServicesTests/NIOTSEventLoopTests.swift b/Tests/NIOTransportServicesTests/NIOTSEventLoopTests.swift index 288a496..0e6797d 100644 --- a/Tests/NIOTransportServicesTests/NIOTSEventLoopTests.swift +++ b/Tests/NIOTransportServicesTests/NIOTSEventLoopTests.swift @@ -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 diff --git a/Tests/NIOTransportServicesTests/NIOTSSingletonTests.swift b/Tests/NIOTransportServicesTests/NIOTSSingletonTests.swift new file mode 100644 index 0000000..0091f08 --- /dev/null +++ b/Tests/NIOTransportServicesTests/NIOTSSingletonTests.swift @@ -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