Skip to content

Commit 1456862

Browse files
Merge pull request #3 from apple/main
NIOSingletons: Use NIO in easy mode (apple#2471)
2 parents f4153f8 + 6c21f51 commit 1456862

File tree

8 files changed

+535
-19
lines changed

8 files changed

+535
-19
lines changed

Package.swift

+4
Original file line numberDiff line numberDiff line change
@@ -393,5 +393,9 @@ let package = Package(
393393
name: "NIOTests",
394394
dependencies: ["NIO"]
395395
),
396+
.testTarget(
397+
name: "NIOSingletonsTests",
398+
dependencies: ["NIOCore", "NIOPosix"]
399+
),
396400
]
397401
)
+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Atomics
16+
#if canImport(Darwin)
17+
import Darwin
18+
#elseif os(Windows)
19+
import ucrt
20+
import WinSDK
21+
#elseif canImport(Glibc)
22+
import Glibc
23+
#elseif canImport(Musl)
24+
import Musl
25+
#else
26+
#error("Unsupported C library")
27+
#endif
28+
29+
/// SwiftNIO provided singleton resources for programs & libraries that don't need full control over all operating
30+
/// system resources. This type holds sizing (how many loops/threads) suggestions.
31+
///
32+
/// Users who need very tight control about the exact threads and resources created may decide to set
33+
/// `NIOSingletons.singletonsEnabledSuggestion = false`. All singleton-creating facilities should check
34+
/// this setting and if `false` restrain from creating any global singleton resources. Please note that disabling the
35+
/// global singletons will lead to a crash if _any_ code attempts to use any of the singletons.
36+
public enum NIOSingletons {
37+
}
38+
39+
extension NIOSingletons {
40+
/// A suggestion of how many ``EventLoop``s the global singleton ``EventLoopGroup``s are supposed to consist of.
41+
///
42+
/// The thread count is ``System/coreCount`` unless the environment variable `NIO_SINGLETON_GROUP_LOOP_COUNT`
43+
/// is set or this value was set manually by the user.
44+
///
45+
/// - note: This value must be set _before_ any singletons are used and must only be set once.
46+
public static var groupLoopCountSuggestion: Int {
47+
set {
48+
Self.userSetSingletonThreadCount(rawStorage: globalRawSuggestedLoopCount, userValue: newValue)
49+
}
50+
51+
get {
52+
return Self.getTrustworthyThreadCount(rawStorage: globalRawSuggestedLoopCount,
53+
environmentVariable: "NIO_SINGLETON_GROUP_LOOP_COUNT")
54+
}
55+
}
56+
57+
/// A suggestion of how many threads the global singleton thread pools that can be used for synchronous, blocking
58+
/// functions (such as `NIOThreadPool`) are supposed to consist of
59+
///
60+
/// The thread count is ``System/coreCount`` unless the environment variable
61+
/// `NIO_SINGLETON_BLOCKING_POOL_THREAD_COUNT` is set or this value was set manually by the user.
62+
///
63+
/// - note: This value must be set _before_ any singletons are used and must only be set once.
64+
public static var blockingPoolThreadCountSuggestion: Int {
65+
set {
66+
Self.userSetSingletonThreadCount(rawStorage: globalRawSuggestedBlockingThreadCount, userValue: newValue)
67+
}
68+
69+
get {
70+
return Self.getTrustworthyThreadCount(rawStorage: globalRawSuggestedBlockingThreadCount,
71+
environmentVariable: "NIO_SINGLETON_BLOCKING_POOL_THREAD_COUNT")
72+
}
73+
}
74+
75+
/// A suggestion for whether the global singletons should be enabled. This is `true` unless changed by the user.
76+
///
77+
/// This value cannot be changed using an environment variable.
78+
///
79+
/// - note: This value must be set _before_ any singletons are used and must only be set once.
80+
public static var singletonsEnabledSuggestion: Bool {
81+
get {
82+
let (exchanged, original) = globalRawSingletonsEnabled.compareExchange(expected: 0,
83+
desired: 1,
84+
ordering: .relaxed)
85+
if exchanged {
86+
// Never been set, we're committing to the default (enabled).
87+
assert(original == 0)
88+
return true
89+
} else {
90+
// This has been set before, 1: enabled; -1 disabled.
91+
assert(original != 0)
92+
assert(original == -1 || original == 1)
93+
return original > 0
94+
}
95+
}
96+
97+
set {
98+
let intRepresentation = newValue ? 1 : -1
99+
let (exchanged, _) = globalRawSingletonsEnabled.compareExchange(expected: 0,
100+
desired: intRepresentation,
101+
ordering: .relaxed)
102+
guard exchanged else {
103+
fatalError("""
104+
Bug in user code: Global singleton enabled suggestion has been changed after \
105+
user or has been changed more than once. Either is an error, you must set this value very \
106+
early and only once.
107+
""")
108+
}
109+
}
110+
}
111+
}
112+
113+
// DO NOT TOUCH THESE DIRECTLY, use `userSetSingletonThreadCount` and `getTrustworthyThreadCount`.
114+
private let globalRawSuggestedLoopCount = ManagedAtomic(0)
115+
private let globalRawSuggestedBlockingThreadCount = ManagedAtomic(0)
116+
private let globalRawSingletonsEnabled = ManagedAtomic(0)
117+
118+
extension NIOSingletons {
119+
private static func userSetSingletonThreadCount(rawStorage: ManagedAtomic<Int>, userValue: Int) {
120+
precondition(userValue > 0, "illegal value: needs to be strictly positive")
121+
122+
// The user is trying to set it. We can only do this if the value is at 0 and we will set the
123+
// negative value. So if the user wants `5`, we will set `-5`. Once it's used (set getter), it'll be upped
124+
// to 5.
125+
let (exchanged, _) = rawStorage.compareExchange(expected: 0, desired: -userValue, ordering: .relaxed)
126+
guard exchanged else {
127+
fatalError("""
128+
Bug in user code: Global singleton suggested loop/thread count has been changed after \
129+
user or has been changed more than once. Either is an error, you must set this value very early \
130+
and only once.
131+
""")
132+
}
133+
}
134+
135+
private static func validateTrustedThreadCount(_ threadCount: Int) {
136+
assert(threadCount > 0,
137+
"BUG IN NIO, please report: negative suggested loop/thread count: \(threadCount)")
138+
assert(threadCount <= 1024,
139+
"BUG IN NIO, please report: overly big suggested loop/thread count: \(threadCount)")
140+
}
141+
142+
private static func getTrustworthyThreadCount(rawStorage: ManagedAtomic<Int>, environmentVariable: String) -> Int {
143+
let returnedValueUnchecked: Int
144+
145+
let rawSuggestion = rawStorage.load(ordering: .relaxed)
146+
switch rawSuggestion {
147+
case 0: // == 0
148+
// Not set by user, not yet finalised, let's try to get it from the env var and fall back to
149+
// `System.coreCount`.
150+
let envVarString = getenv(environmentVariable).map { String(cString: $0) }
151+
returnedValueUnchecked = envVarString.flatMap(Int.init) ?? System.coreCount
152+
case .min ..< 0: // < 0
153+
// Untrusted and unchecked user value. Let's invert and then sanitise/check.
154+
returnedValueUnchecked = -rawSuggestion
155+
case 1 ... .max: // > 0
156+
// Trustworthy value that has been evaluated and sanitised before.
157+
let returnValue = rawSuggestion
158+
Self.validateTrustedThreadCount(returnValue)
159+
return returnValue
160+
default:
161+
// Unreachable
162+
preconditionFailure()
163+
}
164+
165+
// Can't have fewer than 1, don't want more than 1024.
166+
let returnValue = max(1, min(1024, returnedValueUnchecked))
167+
Self.validateTrustedThreadCount(returnValue)
168+
169+
// Store it for next time.
170+
let (exchanged, _) = rawStorage.compareExchange(expected: rawSuggestion,
171+
desired: returnValue,
172+
ordering: .relaxed)
173+
if !exchanged {
174+
// We lost the race, this must mean it has been concurrently set correctly so we can safely recurse
175+
// and try again.
176+
return Self.getTrustworthyThreadCount(rawStorage: rawStorage, environmentVariable: environmentVariable)
177+
}
178+
return returnValue
179+
}
180+
}

Sources/NIOCrashTester/CrashTests+EventLoop.swift

+72
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,77 @@ struct EventLoopCrashTests {
105105
try! el.submit {}.wait()
106106
}
107107
}
108+
109+
let testUsingTheSingletonGroupWhenDisabled = CrashTest(
110+
regex: #"Fatal error: Cannot create global singleton MultiThreadedEventLoopGroup because the global singletons"#
111+
) {
112+
NIOSingletons.singletonsEnabledSuggestion = false
113+
try? NIOSingletons.posixEventLoopGroup.next().submit {}.wait()
114+
}
115+
116+
let testUsingTheSingletonBlockingPoolWhenDisabled = CrashTest(
117+
regex: #"Fatal error: Cannot create global singleton NIOThreadPool because the global singletons have been"#
118+
) {
119+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
120+
defer {
121+
try? group.syncShutdownGracefully()
122+
}
123+
NIOSingletons.singletonsEnabledSuggestion = false
124+
try? NIOSingletons.posixBlockingThreadPool.runIfActive(eventLoop: group.next(), {}).wait()
125+
}
126+
127+
let testDisablingSingletonsEnabledValueTwice = CrashTest(
128+
regex: #"Fatal error: Bug in user code: Global singleton enabled suggestion has been changed after"#
129+
) {
130+
NIOSingletons.singletonsEnabledSuggestion = false
131+
NIOSingletons.singletonsEnabledSuggestion = false
132+
}
133+
134+
let testEnablingSingletonsEnabledValueTwice = CrashTest(
135+
regex: #"Fatal error: Bug in user code: Global singleton enabled suggestion has been changed after"#
136+
) {
137+
NIOSingletons.singletonsEnabledSuggestion = true
138+
NIOSingletons.singletonsEnabledSuggestion = true
139+
}
140+
141+
let testEnablingThenDisablingSingletonsEnabledValue = CrashTest(
142+
regex: #"Fatal error: Bug in user code: Global singleton enabled suggestion has been changed after"#
143+
) {
144+
NIOSingletons.singletonsEnabledSuggestion = true
145+
NIOSingletons.singletonsEnabledSuggestion = false
146+
}
147+
148+
let testSettingTheSingletonEnabledValueAfterUse = CrashTest(
149+
regex: #"Fatal error: Bug in user code: Global singleton enabled suggestion has been changed after"#
150+
) {
151+
try? MultiThreadedEventLoopGroup.singleton.next().submit({}).wait()
152+
NIOSingletons.singletonsEnabledSuggestion = true
153+
}
154+
155+
let testSettingTheSuggestedSingletonGroupCountTwice = CrashTest(
156+
regex: #"Fatal error: Bug in user code: Global singleton suggested loop/thread count has been changed after"#
157+
) {
158+
NIOSingletons.groupLoopCountSuggestion = 17
159+
NIOSingletons.groupLoopCountSuggestion = 17
160+
}
161+
162+
let testSettingTheSuggestedSingletonGroupChangeAfterUse = CrashTest(
163+
regex: #"Fatal error: Bug in user code: Global singleton suggested loop/thread count has been changed after"#
164+
) {
165+
try? MultiThreadedEventLoopGroup.singleton.next().submit({}).wait()
166+
NIOSingletons.groupLoopCountSuggestion = 17
167+
}
168+
169+
let testSettingTheSuggestedSingletonGroupLoopCountToZero = CrashTest(
170+
regex: #"Precondition failed: illegal value: needs to be strictly positive"#
171+
) {
172+
NIOSingletons.groupLoopCountSuggestion = 0
173+
}
174+
175+
let testSettingTheSuggestedSingletonGroupLoopCountToANegativeValue = CrashTest(
176+
regex: #"Precondition failed: illegal value: needs to be strictly positive"#
177+
) {
178+
NIOSingletons.groupLoopCountSuggestion = -1
179+
}
108180
}
109181
#endif

Sources/NIOHTTP1Server/main.swift

+3-13
Original file line numberDiff line numberDiff line change
@@ -514,18 +514,14 @@ default:
514514
bindTarget = BindTo.ip(host: defaultHost, port: defaultPort)
515515
}
516516

517-
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
518-
let threadPool = NIOThreadPool(numberOfThreads: 6)
519-
threadPool.start()
520-
521517
func childChannelInitializer(channel: Channel) -> EventLoopFuture<Void> {
522518
return channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap {
523519
channel.pipeline.addHandler(HTTPHandler(fileIO: fileIO, htdocsPath: htdocs))
524520
}
525521
}
526522

527-
let fileIO = NonBlockingFileIO(threadPool: threadPool)
528-
let socketBootstrap = ServerBootstrap(group: group)
523+
let fileIO = NonBlockingFileIO(threadPool: .singleton)
524+
let socketBootstrap = ServerBootstrap(group: MultiThreadedEventLoopGroup.singleton)
529525
// Specify backlog and enable SO_REUSEADDR for the server itself
530526
.serverChannelOption(ChannelOptions.backlog, value: 256)
531527
.serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
@@ -537,18 +533,12 @@ let socketBootstrap = ServerBootstrap(group: group)
537533
.childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
538534
.childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
539535
.childChannelOption(ChannelOptions.allowRemoteHalfClosure, value: allowHalfClosure)
540-
let pipeBootstrap = NIOPipeBootstrap(group: group)
536+
let pipeBootstrap = NIOPipeBootstrap(group: MultiThreadedEventLoopGroup.singleton)
541537
// Set the handlers that are applied to the accepted Channels
542538
.channelInitializer(childChannelInitializer(channel:))
543539

544540
.channelOption(ChannelOptions.maxMessagesPerRead, value: 1)
545541
.channelOption(ChannelOptions.allowRemoteHalfClosure, value: allowHalfClosure)
546-
547-
defer {
548-
try! group.syncShutdownGracefully()
549-
try! threadPool.syncShutdownGracefully()
550-
}
551-
552542
print("htdocs = \(htdocs)")
553543

554544
let channel = try { () -> Channel in

0 commit comments

Comments
 (0)