Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -393,5 +393,9 @@ let package = Package(
name: "NIOTests",
dependencies: ["NIO"]
),
.testTarget(
name: "NIOSingletonsTests",
dependencies: ["NIOCore", "NIOPosix"]
),
]
)
180 changes: 180 additions & 0 deletions Sources/NIOCore/GlobalSingletons.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
//===----------------------------------------------------------------------===//
//
// 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 Atomics
#if canImport(Darwin)
import Darwin
#elseif os(Windows)
import ucrt
import WinSDK
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Musl)
import Musl
#else
#error("Unsupported C library")
#endif

/// SwiftNIO provided singleton resources for programs & libraries that don't need full control over all operating
/// system resources. This type holds sizing (how many loops/threads) suggestions.
///
/// Users who need very tight control about the exact threads and resources created may decide to set
/// `NIOSingletons.singletonsEnabledSuggestion = false`. All singleton-creating facilities should check
/// this setting and if `false` restrain from creating any global singleton resources. Please note that disabling the
/// global singletons will lead to a crash if _any_ code attempts to use any of the singletons.
public enum NIOSingletons {
}

extension NIOSingletons {
/// A suggestion of how many ``EventLoop``s the global singleton ``EventLoopGroup``s are supposed to consist of.
///
/// The thread count is ``System/coreCount`` unless the environment variable `NIO_SINGLETON_GROUP_LOOP_COUNT`
/// is set or this value was set manually by the user.
///
/// - note: This value must be set _before_ any singletons are used and must only be set once.
public static var groupLoopCountSuggestion: Int {
set {
Self.userSetSingletonThreadCount(rawStorage: globalRawSuggestedLoopCount, userValue: newValue)
}

get {
return Self.getTrustworthyThreadCount(rawStorage: globalRawSuggestedLoopCount,
environmentVariable: "NIO_SINGLETON_GROUP_LOOP_COUNT")
}
}

/// A suggestion of how many threads the global singleton thread pools that can be used for synchronous, blocking
/// functions (such as ``NIOThreadPool``) are supposed to consist of
///
/// The thread count is ``System/coreCount`` unless the environment variable
/// `NIO_SINGLETON_BLOCKING_POOL_THREAD_COUNT` is set or this value was set manually by the user.
///
/// - note: This value must be set _before_ any singletons are used and must only be set once.
public static var blockingPoolThreadCountSuggestion: Int {
set {
Self.userSetSingletonThreadCount(rawStorage: globalRawSuggestedBlockingThreadCount, userValue: newValue)
}

get {
return Self.getTrustworthyThreadCount(rawStorage: globalRawSuggestedBlockingThreadCount,
environmentVariable: "NIO_SINGLETON_BLOCKING_POOL_THREAD_COUNT")
}
}

/// A suggestion for whether the global singletons should be enabled. This is `true` unless changed by the user.
///
/// This value cannot be changed using an environment variable.
///
/// - note: This value must be set _before_ any singletons are used and must only be set once.
public static var singletonsEnabledSuggestion: Bool {
get {
let (exchanged, original) = globalRawSingletonsEnabled.compareExchange(expected: 0,
desired: 1,
ordering: .relaxed)
if exchanged {
// Never been set, we're committing to the default (enabled).
assert(original == 0)
return true
} else {
// This has been set before, 1: enabled; -1 disabled.
assert(original != 0)
assert(original == -1 || original == 1)
return original > 0
}
}

set {
let intRepresentation = newValue ? 1 : -1
let (exchanged, _) = globalRawSingletonsEnabled.compareExchange(expected: 0,
desired: intRepresentation,
ordering: .relaxed)
guard exchanged else {
fatalError("""
Bug in user code: Global singleton enabled suggestion has been changed after \
user or has been changed more than once. Either is an error, you must set this value very \
early and only once.
""")
}
}
}
}

// DO NOT TOUCH THESE DIRECTLY, use `userSetSingletonThreadCount` and `getTrustworthyThreadCount`.
private let globalRawSuggestedLoopCount = ManagedAtomic(0)
private let globalRawSuggestedBlockingThreadCount = ManagedAtomic(0)
private let globalRawSingletonsEnabled = ManagedAtomic(0)

extension NIOSingletons {
private static func userSetSingletonThreadCount(rawStorage: ManagedAtomic<Int>, userValue: Int) {
precondition(userValue > 0, "illegal value: needs to be strictly positive")

// The user is trying to set it. We can only do this if the value is at 0 and we will set the
// negative value. So if the user wants `5`, we will set `-5`. Once it's used (set getter), it'll be upped
// to 5.
let (exchanged, _) = rawStorage.compareExchange(expected: 0, desired: -userValue, ordering: .relaxed)
guard exchanged else {
fatalError("""
Bug in user code: Global singleton suggested loop/thread count has been changed after \
user or has been changed more than once. Either is an error, you must set this value very early \
and only once.
""")
}
}

private static func validateTrustedThreadCount(_ threadCount: Int) {
assert(threadCount > 0,
"BUG IN NIO, please report: negative suggested loop/thread count: \(threadCount)")
assert(threadCount <= 1024,
"BUG IN NIO, please report: overly big suggested loop/thread count: \(threadCount)")
}

private static func getTrustworthyThreadCount(rawStorage: ManagedAtomic<Int>, environmentVariable: String) -> Int {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lowkey love how we solved this!

let returnedValueUnchecked: Int

let rawSuggestion = rawStorage.load(ordering: .relaxed)
switch rawSuggestion {
case 0: // == 0
// Not set by user, not yet finalised, let's try to get it from the env var and fall back to
// `System.coreCount`.
let envVarString = getenv(environmentVariable).map { String(cString: $0) }
returnedValueUnchecked = envVarString.flatMap(Int.init) ?? System.coreCount
case .min ..< 0: // < 0
// Untrusted and unchecked user value. Let's invert and then sanitise/check.
returnedValueUnchecked = -rawSuggestion
case 1 ... .max: // > 0
// Trustworthy value that has been evaluated and sanitised before.
let returnValue = rawSuggestion
Self.validateTrustedThreadCount(returnValue)
return returnValue
default:
// Unreachable
preconditionFailure()
}

// Can't have fewer than 1, don't want more than 1024.
let returnValue = max(1, min(1024, returnedValueUnchecked))
Self.validateTrustedThreadCount(returnValue)

// Store it for next time.
let (exchanged, _) = rawStorage.compareExchange(expected: rawSuggestion,
desired: returnValue,
ordering: .relaxed)
if !exchanged {
// We lost the race, this must mean it has been concurrently set correctly so we can safely recurse
// and try again.
return Self.getTrustworthyThreadCount(rawStorage: rawStorage, environmentVariable: environmentVariable)
}
return returnValue
}
}
72 changes: 72 additions & 0 deletions Sources/NIOCrashTester/CrashTests+EventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,77 @@ struct EventLoopCrashTests {
try! el.submit {}.wait()
}
}

let testUsingTheSingletonGroupWhenDisabled = CrashTest(
regex: #"Fatal error: Cannot create global singleton MultiThreadedEventLoopGroup because the global singletons"#
) {
NIOSingletons.singletonsEnabledSuggestion = false
try? NIOSingletons.posixEventLoopGroup.next().submit {}.wait()
}

let testUsingTheSingletonBlockingPoolWhenDisabled = CrashTest(
regex: #"Fatal error: Cannot create global singleton NIOThreadPool because the global singletons have been"#
) {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
try? group.syncShutdownGracefully()
}
NIOSingletons.singletonsEnabledSuggestion = false
try? NIOSingletons.posixBlockingThreadPool.runIfActive(eventLoop: group.next(), {}).wait()
}

let testDisablingSingletonsEnabledValueTwice = CrashTest(
regex: #"Fatal error: Bug in user code: Global singleton enabled suggestion has been changed after"#
) {
NIOSingletons.singletonsEnabledSuggestion = false
NIOSingletons.singletonsEnabledSuggestion = false
}

let testEnablingSingletonsEnabledValueTwice = CrashTest(
regex: #"Fatal error: Bug in user code: Global singleton enabled suggestion has been changed after"#
) {
NIOSingletons.singletonsEnabledSuggestion = true
NIOSingletons.singletonsEnabledSuggestion = true
}

let testEnablingThenDisablingSingletonsEnabledValue = CrashTest(
regex: #"Fatal error: Bug in user code: Global singleton enabled suggestion has been changed after"#
) {
NIOSingletons.singletonsEnabledSuggestion = true
NIOSingletons.singletonsEnabledSuggestion = false
}

let testSettingTheSingletonEnabledValueAfterUse = CrashTest(
regex: #"Fatal error: Bug in user code: Global singleton enabled suggestion has been changed after"#
) {
try? MultiThreadedEventLoopGroup.singleton.next().submit({}).wait()
NIOSingletons.singletonsEnabledSuggestion = true
}

let testSettingTheSuggestedSingletonGroupCountTwice = CrashTest(
regex: #"Fatal error: Bug in user code: Global singleton suggested loop/thread count has been changed after"#
) {
NIOSingletons.groupLoopCountSuggestion = 17
NIOSingletons.groupLoopCountSuggestion = 17
}

let testSettingTheSuggestedSingletonGroupChangeAfterUse = CrashTest(
regex: #"Fatal error: Bug in user code: Global singleton suggested loop/thread count has been changed after"#
) {
try? MultiThreadedEventLoopGroup.singleton.next().submit({}).wait()
NIOSingletons.groupLoopCountSuggestion = 17
}

let testSettingTheSuggestedSingletonGroupLoopCountToZero = CrashTest(
regex: #"Precondition failed: illegal value: needs to be strictly positive"#
) {
NIOSingletons.groupLoopCountSuggestion = 0
}

let testSettingTheSuggestedSingletonGroupLoopCountToANegativeValue = CrashTest(
regex: #"Precondition failed: illegal value: needs to be strictly positive"#
) {
NIOSingletons.groupLoopCountSuggestion = -1
}
}
#endif
16 changes: 3 additions & 13 deletions Sources/NIOHTTP1Server/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,18 +514,14 @@ default:
bindTarget = BindTo.ip(host: defaultHost, port: defaultPort)
}

let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let threadPool = NIOThreadPool(numberOfThreads: 6)
threadPool.start()

func childChannelInitializer(channel: Channel) -> EventLoopFuture<Void> {
return channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap {
channel.pipeline.addHandler(HTTPHandler(fileIO: fileIO, htdocsPath: htdocs))
}
}

let fileIO = NonBlockingFileIO(threadPool: threadPool)
let socketBootstrap = ServerBootstrap(group: group)
let fileIO = NonBlockingFileIO(threadPool: .singleton)
let socketBootstrap = ServerBootstrap(group: MultiThreadedEventLoopGroup.singleton)
// Specify backlog and enable SO_REUSEADDR for the server itself
.serverChannelOption(ChannelOptions.backlog, value: 256)
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
Expand All @@ -537,18 +533,12 @@ let socketBootstrap = ServerBootstrap(group: group)
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
.childChannelOption(ChannelOptions.allowRemoteHalfClosure, value: allowHalfClosure)
let pipeBootstrap = NIOPipeBootstrap(group: group)
let pipeBootstrap = NIOPipeBootstrap(group: MultiThreadedEventLoopGroup.singleton)
// Set the handlers that are applied to the accepted Channels
.channelInitializer(childChannelInitializer(channel:))

.channelOption(ChannelOptions.maxMessagesPerRead, value: 1)
.channelOption(ChannelOptions.allowRemoteHalfClosure, value: allowHalfClosure)

defer {
try! group.syncShutdownGracefully()
try! threadPool.syncShutdownGracefully()
}

print("htdocs = \(htdocs)")

let channel = try { () -> Channel in
Expand Down
Loading