Skip to content

Commit

Permalink
NIOSingletons: Use NIO in easy mode
Browse files Browse the repository at this point in the history
  • Loading branch information
weissi committed Jul 13, 2023
1 parent 5f54289 commit 5334af4
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 2 deletions.
19 changes: 19 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ let package = Package(
.library(name: "NIOFoundationCompat", targets: ["NIOFoundationCompat"]),
.library(name: "NIOWebSocket", targets: ["NIOWebSocket"]),
.library(name: "NIOTestUtils", targets: ["NIOTestUtils"]),
.library(name: "NIOSingletonsPosix", targets: ["NIOSingletonsPosix"]),
.library(name: "NIOSingletonResources", targets: ["NIOSingletonResources"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0"),
Expand Down Expand Up @@ -168,6 +170,19 @@ let package = Package(
swiftAtomics,
]
),
.target(
name: "NIOSingletonResources",
dependencies: [
"NIOCore",
]
),
.target(
name: "NIOSingletonsPosix",
dependencies: [
"NIOSingletonResources",
"NIOPosix",
]
),

// MARK: - Examples

Expand Down Expand Up @@ -393,5 +408,9 @@ let package = Package(
name: "NIOTests",
dependencies: ["NIO"]
),
.testTarget(
name: "NIOSingletonsTests",
dependencies: ["NIOSingletonResources", "NIOSingletonsPosix", "NIOCore"]
),
]
)
34 changes: 32 additions & 2 deletions Sources/NIOPosix/MultiThreadedEventLoopGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public final class MultiThreadedEventLoopGroup: EventLoopGroup {
private var eventLoops: [SelectableEventLoop]
private let shutdownLock: NIOLock = NIOLock()
private var runState: RunState = .running
private let canBeShutDown: Bool

private static func runTheLoop(thread: NIOThread,
parentGroup: MultiThreadedEventLoopGroup? /* nil iff thread take-over */,
Expand Down Expand Up @@ -138,25 +139,48 @@ public final class MultiThreadedEventLoopGroup: EventLoopGroup {
/// - arguments:
/// - numberOfThreads: The number of `Threads` to use.
public convenience init(numberOfThreads: Int) {
self.init(numberOfThreads: numberOfThreads, selectorFactory: NIOPosix.Selector<NIORegistration>.init)
self.init(numberOfThreads: numberOfThreads,
canBeShutDown: true,
selectorFactory: NIOPosix.Selector<NIORegistration>.init)
}

public convenience init(_canBeShutDown canBeShutDown: Bool, numberOfThreads: Int) {
self.init(numberOfThreads: numberOfThreads,
canBeShutDown: canBeShutDown,
selectorFactory: NIOPosix.Selector<NIORegistration>.init)
}

internal convenience init(numberOfThreads: Int,
selectorFactory: @escaping () throws -> NIOPosix.Selector<NIORegistration>) {
precondition(numberOfThreads > 0, "numberOfThreads must be positive")
let initializers: [ThreadInitializer] = Array(repeating: { _ in }, count: numberOfThreads)
self.init(threadInitializers: initializers, canBeShutDown: true, selectorFactory: selectorFactory)
}

internal convenience init(numberOfThreads: Int,
canBeShutDown: Bool,
selectorFactory: @escaping () throws -> NIOPosix.Selector<NIORegistration>) {
precondition(numberOfThreads > 0, "numberOfThreads must be positive")
let initializers: [ThreadInitializer] = Array(repeating: { _ in }, count: numberOfThreads)
self.init(threadInitializers: initializers, selectorFactory: selectorFactory)
self.init(threadInitializers: initializers, canBeShutDown: canBeShutDown, selectorFactory: selectorFactory)
}

internal convenience init(threadInitializers: [ThreadInitializer],
selectorFactory: @escaping () throws -> NIOPosix.Selector<NIORegistration> = NIOPosix.Selector<NIORegistration>.init) {
self.init(threadInitializers: threadInitializers, canBeShutDown: true, selectorFactory: selectorFactory)
}

/// Creates a `MultiThreadedEventLoopGroup` instance which uses the given `ThreadInitializer`s. One `NIOThread` per `ThreadInitializer` is created and used.
///
/// - arguments:
/// - threadInitializers: The `ThreadInitializer`s to use.
internal init(threadInitializers: [ThreadInitializer],
canBeShutDown: Bool,
selectorFactory: @escaping () throws -> NIOPosix.Selector<NIORegistration> = NIOPosix.Selector<NIORegistration>.init) {
let myGroupID = nextEventLoopGroupID.loadThenWrappingIncrement(ordering: .relaxed)
self.myGroupID = myGroupID
var idx = 0
self.canBeShutDown = canBeShutDown
self.eventLoops = [] // Just so we're fully initialised and can vend `self` to the `SelectableEventLoop`.
self.eventLoops = threadInitializers.map { initializer in
// Maximum name length on linux is 16 by default.
Expand Down Expand Up @@ -244,6 +268,12 @@ public final class MultiThreadedEventLoopGroup: EventLoopGroup {
#endif

private func _shutdownGracefully(queue: DispatchQueue, _ handler: @escaping ShutdownGracefullyCallback) {
guard self.canBeShutDown else {
queue.async {
handler(EventLoopError.unsupportedOperation)
}
return
}
// This method cannot perform its final cleanup using EventLoopFutures, because it requires that all
// our event loops still be alive, and they may not be. Instead, we use Dispatch to manage
// our shutdown signaling, and then do our cleanup once the DispatchQueue is empty.
Expand Down
74 changes: 74 additions & 0 deletions Sources/NIOSingletonResources/SingletonType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//===----------------------------------------------------------------------===//
//
// 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(Darwin)
import Darwin
#elseif os(Windows)
import ucrt
import WinSDK
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#else
#error("The concurrency NIOLock module was unable to identify your C library.")
#endif
import NIOCore

/// Singleton resources provided by SwiftNIO for programs & libraries that don't need full control over all operating system resources.
///
/// 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 the 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.
public enum NIOSingletons {}

extension NIOSingletons {
/// How many threads will be used for the multi-threaded `EventLoopGroup`s.
///
/// Uses `NIO_SINGLETONS_MULTI_THREADED_ELG_THREAD_COUNT`.
public static var suggestedMultiThreadedEventLoopGroupThreadCount: Int {
return globalSuggestedMultiThreadedEventLoopGroupThreadCount
}

/// How many threads will be used for the blocking `NIOThreadPool`s.
///
/// Uses `NIO_SINGLETONS_BLOCKING_POOL_THREAD_COUNT`.
public static var suggestedBlockingPoolThreadCount: Int {
return globalSuggestedBlockingPoolThreadCount
}
}

private let globalSuggestedMultiThreadedEventLoopGroupThreadCount: Int = {
if let threadCountEnv = getenv("NIO_SINGLETONS_MULTI_THREADED_ELG_THREAD_COUNT"),
let threadCount = Int(String(cString: threadCountEnv)), threadCount > 0 {
return threadCount
} else {
return System.coreCount
}
}()

private let globalSuggestedBlockingPoolThreadCount: Int = {
if let threadCountEnv = getenv("NIO_SINGLETONS_BLOCKING_POOL_THREAD_COUNT"),
let threadCount = Int(String(cString: threadCountEnv)), threadCount > 0 {
return threadCount
} else {
return System.coreCount * 2
}
}()

61 changes: 61 additions & 0 deletions Sources/NIOSingletonsPosix/PosixSingletons.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

@_exported import NIOSingletonResources
import NIOPosix
import NIOCore

extension NIOSingletons {
/// A globally shared, lazily intialized EventLoopGroup that uses `epll` as the selector mechanism.
public static var multiThreadedPosixEventLoopGroup: MultiThreadedEventLoopGroup {
return globalMultiThreadedPosixEventLoopGroup
}

/// A globally shared, lazily intialized EventLoopGroup with just one thread that uses `epoll` as the selector mechanism.
public static var singleThreadedPosixEventLoopGroup: MultiThreadedEventLoopGroup {
return globalSingleThreadedPosixEventLoopGroup
}

/// A globally shared, lazily intialized EventLoop that uses `epoll` as the selector mechanism. Backed by `NIOSingletons.singleThreadedPosixEventLoopGroup`.
public static var posixEventLoop: EventLoop {
return Self.singleThreadedPosixEventLoopGroup.next()
}

/// A globally shared, lazily intialized `NIOThreadPool` that can be used for blocking I/O and other blocking operations.
public static var posixBlockingPool: NIOThreadPool {
return globalPosixBlockingPool
}
}

private let globalMultiThreadedPosixEventLoopGroup: MultiThreadedEventLoopGroup = {
let group = MultiThreadedEventLoopGroup(_canBeShutDown: false,
numberOfThreads: NIOSingletons.suggestedMultiThreadedEventLoopGroupThreadCount)
_ = Unmanaged.passUnretained(group).retain() // Never gonna give you up,
return group
}()

private let globalSingleThreadedPosixEventLoopGroup: MultiThreadedEventLoopGroup = {
let group = MultiThreadedEventLoopGroup(_canBeShutDown: false, numberOfThreads: 1)
_ = Unmanaged.passUnretained(group).retain() // never gonna let you down.
return group
}()

private let globalPosixBlockingPool: NIOThreadPool = {
let pool = NIOThreadPool(numberOfThreads: NIOSingletons.suggestedBlockingPoolThreadCount)
pool.start()
_ = Unmanaged.passUnretained(pool).retain() // Make sure this is never deallocated (not strictly necessary).
return pool
}()


68 changes: 68 additions & 0 deletions Tests/NIOSingletonsTests/SingletonsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

import XCTest
import NIOSingletonResources
import NIOSingletonsPosix
import NIOCore

final class NIOSingletonsTests: XCTestCase {
func testSingleThreadedPosixGroupHasOneThread() {
XCTAssertEqual(1, Array(NIOSingletons.singleThreadedPosixEventLoopGroup.makeIterator()).count)
}

func testSingleThreadedAndMultiThreadedPosixGroupsAreDifferent() {
XCTAssert(NIOSingletons.singleThreadedPosixEventLoopGroup !== NIOSingletons.multiThreadedPosixEventLoopGroup)
}

func testSingletonEventLoopWorks() async throws {
let works = try await NIOSingletons.posixEventLoop.submit { "yes" }.get()
XCTAssertEqual(works, "yes")
}

func testSingletonMultiThreadedEventLoopWorks() async throws {
let works = try await NIOSingletons.multiThreadedPosixEventLoopGroup.any().submit { "yes" }.get()
XCTAssertEqual(works, "yes")
}

func testSingletonSingleThreadedEventLoopWorks() async throws {
let works = try await NIOSingletons.singleThreadedPosixEventLoopGroup.any().submit { "yes" }.get()
XCTAssertEqual(works, "yes")
}

func testSingletonBlockingPoolWorks() async throws {
let works = try await NIOSingletons.posixBlockingPool.runIfActive(eventLoop: NIOSingletons.posixEventLoop) {
"yes"
}.get()
XCTAssertEqual(works, "yes")
}

func testCannotShutdownMultiGroup() {
XCTAssertThrowsError(try NIOSingletons.multiThreadedPosixEventLoopGroup.syncShutdownGracefully()) { error in
XCTAssertEqual(.unsupportedOperation, error as? EventLoopError)
}
}

func testCannotShutdownSingleGroup() {
XCTAssertThrowsError(try NIOSingletons.singleThreadedPosixEventLoopGroup.syncShutdownGracefully()) { error in
XCTAssertEqual(.unsupportedOperation, error as? EventLoopError)
}
}

func testCannotShutdownSingleLoop() {
XCTAssertThrowsError(try NIOSingletons.posixEventLoop.syncShutdownGracefully()) { error in
XCTAssertEqual(.unsupportedOperation, error as? EventLoopError)
}
}
}

0 comments on commit 5334af4

Please sign in to comment.