Coverage Report

Created: 2026-02-11 06:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/swift-nio/Sources/NIOCore/GlobalSingletons.swift
Line
Count
Source
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
17
#if canImport(Darwin)
18
import Darwin
19
#elseif os(Windows)
20
import ucrt
21
import WinSDK
22
#elseif canImport(Glibc)
23
@preconcurrency import Glibc
24
#elseif canImport(Musl)
25
@preconcurrency import Musl
26
#elseif canImport(Bionic)
27
@preconcurrency import Bionic
28
#elseif canImport(WASILibc)
29
@preconcurrency import WASILibc
30
#else
31
#error("Unsupported C library")
32
#endif
33
34
/// SwiftNIO provided singleton resources for programs & libraries that don't need full control over all operating
35
/// system resources. This type holds sizing (how many loops/threads) suggestions.
36
///
37
/// Users who need very tight control about the exact threads and resources created may decide to set
38
/// `NIOSingletons.singletonsEnabledSuggestion = false`. All singleton-creating facilities should check
39
/// this setting and if `false` restrain from creating any global singleton resources. Please note that disabling the
40
/// global singletons will lead to a crash if _any_ code attempts to use any of the singletons.
41
public enum NIOSingletons: Sendable {
42
}
43
44
extension NIOSingletons {
45
    /// A suggestion of how many ``EventLoop``s the global singleton ``EventLoopGroup``s are supposed to consist of.
46
    ///
47
    /// The thread count is ``System/coreCount`` unless the environment variable `NIO_SINGLETON_GROUP_LOOP_COUNT`
48
    /// is set or this value was set manually by the user.
49
    ///
50
    /// Please note that setting this value is a privileged operation which should be performed very early on in the program's lifecycle
51
    /// by the main function, or ideally not at all.
52
    /// Furthermore, setting the value will only have an effect if the global singleton ``EventLoopGroup`` has not already
53
    /// been used.
54
    ///
55
    /// - Warning: This value can only be set _once_, attempts to set it again will crash the program.
56
    /// - Note: This value must be set _before_ any singletons are used and must only be set once.
57
    public static var groupLoopCountSuggestion: Int {
58
0
        set {
59
0
            Self.userSetSingletonThreadCount(rawStorage: globalRawSuggestedLoopCount, userValue: newValue)
60
0
        }
61
62
0
        get {
63
0
            Self.getTrustworthyThreadCount(
64
0
                rawStorage: globalRawSuggestedLoopCount,
65
0
                environmentVariable: "NIO_SINGLETON_GROUP_LOOP_COUNT"
66
0
            )
67
0
        }
68
    }
69
70
    /// A suggestion of how many threads the global singleton thread pools that can be used for synchronous, blocking
71
    /// functions (such as `NIOThreadPool`) are supposed to consist of
72
    ///
73
    /// The thread count is ``System/coreCount`` unless the environment variable
74
    /// `NIO_SINGLETON_BLOCKING_POOL_THREAD_COUNT` is set or this value was set manually by the user.
75
    ///
76
    /// Please note that setting this value is a privileged operation which should be performed very early on in the program's lifecycle
77
    /// by the main function, or ideally not at all.
78
    /// Furthermore, setting the value will only have an effect if the global singleton thread pool has not already
79
    /// been used.
80
    ///
81
    /// - Warning: This value can only be set _once_, attempts to set it again will crash the program.
82
    /// - Note: This value must be set _before_ any singletons are used and must only be set once.
83
    public static var blockingPoolThreadCountSuggestion: Int {
84
0
        set {
85
0
            Self.userSetSingletonThreadCount(rawStorage: globalRawSuggestedBlockingThreadCount, userValue: newValue)
86
0
        }
87
88
0
        get {
89
0
            Self.getTrustworthyThreadCount(
90
0
                rawStorage: globalRawSuggestedBlockingThreadCount,
91
0
                environmentVariable: "NIO_SINGLETON_BLOCKING_POOL_THREAD_COUNT"
92
0
            )
93
0
        }
94
    }
95
96
    /// A suggestion for whether the global singletons should be enabled. This is `true` unless changed by the user.
97
    ///
98
    /// This value cannot be changed using an environment variable.
99
    ///
100
    /// Please note that setting this value is a privileged operation which should be performed very early on in the program's lifecycle
101
    /// by the main function, or ideally not at all.
102
    ///
103
    /// - Warning: This value can only be set _once_, attempts to set it again will crash the program.
104
    /// - Note: This value must be set _before_ any singletons are used and must only be set once.
105
    public static var singletonsEnabledSuggestion: Bool {
106
0
        get {
107
0
            let (exchanged, original) = globalRawSingletonsEnabled.compareExchange(
108
0
                expected: 0,
109
0
                desired: 1,
110
0
                ordering: .relaxed
111
0
            )
112
0
            if exchanged {
113
0
                // Never been set, we're committing to the default (enabled).
114
0
                assert(original == 0)
115
0
                return true
116
0
            } else {
117
0
                // This has been set before, 1: enabled; -1 disabled.
118
0
                assert(original != 0)
119
0
                assert(original == -1 || original == 1)
120
0
                return original > 0
121
0
            }
122
0
        }
123
124
0
        set {
125
0
            let intRepresentation = newValue ? 1 : -1
126
0
            let (exchanged, _) = globalRawSingletonsEnabled.compareExchange(
127
0
                expected: 0,
128
0
                desired: intRepresentation,
129
0
                ordering: .relaxed
130
0
            )
131
0
            guard exchanged else {
132
0
                fatalError(
133
0
                    """
134
0
                    Bug in user code: Global singleton enabled suggestion has been changed after \
135
0
                    user or has been changed more than once. Either is an error, you must set this value very \
136
0
                    early and only once.
137
0
                    """
138
0
                )
139
0
            }
140
0
        }
141
    }
142
}
143
144
// DO NOT TOUCH THESE DIRECTLY, use `userSetSingletonThreadCount` and `getTrustworthyThreadCount`.
145
private let globalRawSuggestedLoopCount = ManagedAtomic(0)
146
private let globalRawSuggestedBlockingThreadCount = ManagedAtomic(0)
147
private let globalRawSingletonsEnabled = ManagedAtomic(0)
148
149
extension NIOSingletons {
150
0
    private static func userSetSingletonThreadCount(rawStorage: ManagedAtomic<Int>, userValue: Int) {
151
0
        precondition(userValue > 0, "illegal value: needs to be strictly positive")
152
0
153
0
        // The user is trying to set it. We can only do this if the value is at 0 and we will set the
154
0
        // negative value. So if the user wants `5`, we will set `-5`. Once it's used (set getter), it'll be upped
155
0
        // to 5.
156
0
        let (exchanged, _) = rawStorage.compareExchange(expected: 0, desired: -userValue, ordering: .relaxed)
157
0
        guard exchanged else {
158
0
            fatalError(
159
0
                """
160
0
                Bug in user code: Global singleton suggested loop/thread count has been changed after \
161
0
                user or has been changed more than once. Either is an error, you must set this value very early \
162
0
                and only once.
163
0
                """
164
0
            )
165
0
        }
166
0
    }
167
168
0
    private static func validateTrustedThreadCount(_ threadCount: Int) {
169
0
        assert(
170
0
            threadCount > 0,
171
0
            "BUG IN NIO, please report: negative suggested loop/thread count: \(threadCount)"
172
0
        )
173
0
        assert(
174
0
            threadCount <= 1024,
175
0
            "BUG IN NIO, please report: overly big suggested loop/thread count: \(threadCount)"
176
0
        )
177
0
    }
178
179
0
    private static func getTrustworthyThreadCount(rawStorage: ManagedAtomic<Int>, environmentVariable: String) -> Int {
180
0
        let returnedValueUnchecked: Int
181
0
182
0
        let rawSuggestion = rawStorage.load(ordering: .relaxed)
183
0
        switch rawSuggestion {
184
0
        case 0:  // == 0
185
0
            // Not set by user, not yet finalised, let's try to get it from the env var and fall back to
186
0
            // `System.coreCount`.
187
            #if os(Windows)
188
            let envVarString = Windows.getenv(environmentVariable)
189
            #else
190
0
            let envVarString = getenv(environmentVariable).map { String(cString: $0) }
191
            #endif
192
0
            returnedValueUnchecked = envVarString.flatMap(Int.init) ?? System.coreCount
193
0
        case .min..<0:  // < 0
194
0
            // Untrusted and unchecked user value. Let's invert and then sanitise/check.
195
0
            returnedValueUnchecked = -rawSuggestion
196
0
        case 1 ... .max:  // > 0
197
0
            // Trustworthy value that has been evaluated and sanitised before.
198
0
            let returnValue = rawSuggestion
199
0
            Self.validateTrustedThreadCount(returnValue)
200
0
            return returnValue
201
0
        default:
202
0
            // Unreachable
203
0
            preconditionFailure()
204
0
        }
205
0
206
0
        // Can't have fewer than 1, don't want more than 1024.
207
0
        let returnValue = max(1, min(1024, returnedValueUnchecked))
208
0
        Self.validateTrustedThreadCount(returnValue)
209
0
210
0
        // Store it for next time.
211
0
        let (exchanged, _) = rawStorage.compareExchange(
212
0
            expected: rawSuggestion,
213
0
            desired: returnValue,
214
0
            ordering: .relaxed
215
0
        )
216
0
        if !exchanged {
217
0
            // We lost the race, this must mean it has been concurrently set correctly so we can safely recurse
218
0
            // and try again.
219
0
            return Self.getTrustworthyThreadCount(rawStorage: rawStorage, environmentVariable: environmentVariable)
220
0
        }
221
0
        return returnValue
222
0
    }
223
}