Coverage Report

Created: 2026-07-29 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/swift-nio/Sources/NIOPosix/MultiThreadedEventLoopGroup.swift
Line
Count
Source
1
//===----------------------------------------------------------------------===//
2
//
3
// This source file is part of the SwiftNIO open source project
4
//
5
// Copyright (c) 2017-2024 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
#if !os(WASI)
16
17
import Atomics
18
import CNIOPosix
19
import NIOConcurrencyHelpers
20
import NIOCore
21
22
#if canImport(Dispatch)
23
import Dispatch
24
#endif
25
26
@usableFromInline
27
struct NIORegistration: Registration {
28
    enum ChannelType {
29
        case serverSocketChannel(ServerSocketChannel)
30
        case socketChannel(SocketChannel)
31
        case datagramChannel(DatagramChannel)
32
        case pipeChannel(PipeChannel, PipeChannel.Direction)
33
    }
34
35
    var channel: ChannelType
36
37
    /// The `SelectorEventSet` in which this `NIORegistration` is interested in.
38
    @usableFromInline
39
    var interested: SelectorEventSet
40
41
    /// The registration ID for this `NIORegistration` used by the `Selector`.
42
    @usableFromInline
43
    var registrationID: SelectorRegistrationID
44
}
45
46
@available(*, unavailable)
47
extension NIORegistration: Sendable {}
48
49
/// Called per `NIOThread` that is created for an EventLoop to do custom initialization of the `NIOThread` before the actual `EventLoop` is run on it.
50
typealias ThreadInitializer = (NIOThread) -> Void
51
52
/// An `EventLoopGroup` which will create multiple `EventLoop`s, each tied to its own `NIOThread`.
53
///
54
/// The effect of initializing a `MultiThreadedEventLoopGroup` is to spawn `numberOfThreads` fresh threads which will
55
/// all run their own `EventLoop`. Those threads will not be shut down until `shutdownGracefully` or
56
/// `syncShutdownGracefully` is called.
57
///
58
/// - warning: Unit tests often spawn one `MultiThreadedEventLoopGroup` per unit test to force isolation between the
59
///            tests. In those cases it's important to shut the `MultiThreadedEventLoopGroup` down at the end of the
60
///            test. A good place to start a `MultiThreadedEventLoopGroup` is the `setUp` method of your `XCTestCase`
61
///            subclass, a good place to shut it down is the `tearDown` method.
62
public final class MultiThreadedEventLoopGroup: EventLoopGroup {
63
    typealias _ShutdownGracefullyCallback = @Sendable (Error?) -> Void
64
65
    private enum RunState {
66
        case running
67
        case closing([(DispatchQueue, _ShutdownGracefullyCallback)])
68
        case closed(Error?)
69
    }
70
71
    internal enum _CanBeShutDown {
72
        case yes
73
        case no
74
        case notByUser
75
    }
76
77
    private let myGroupID: Int
78
0
    private let index = ManagedAtomic<Int>(0)
79
    private var eventLoops: [SelectableEventLoop]
80
0
    private let shutdownLock: NIOLock = NIOLock()
81
    private let threadNamePrefix: String
82
0
    private var runState: RunState = .running
83
    private let canBeShutDown: _CanBeShutDown
84
85
0
    private static func storeLoopThreadLocalReference(to loop: SelectableEventLoop) {
86
0
        let existingRef = c_nio_posix_get_el_ptr()
87
0
        precondition(
88
0
            existingRef == nil,
89
0
            "weird, current SEL reference \(String(describing: existingRef)), expected nil"
90
0
        )
91
0
92
0
        let newRef = Unmanaged.passRetained(loop).toOpaque()
93
0
        c_nio_posix_set_el_ptr(newRef)
94
0
    }
95
96
0
    private static func clearLoopThreadLocalReference() {
97
0
        let existingRef = c_nio_posix_get_el_ptr()
98
0
        precondition(existingRef != nil, "weird, current SEL reference is unexpectedly nil")
99
0
100
0
        Unmanaged<SelectableEventLoop>.fromOpaque(existingRef!).release()
101
0
        c_nio_posix_set_el_ptr(nil)
102
0
    }
103
104
    private static func runTheLoop(
105
        thread: NIOThread,
106
        uniqueID: SelectableEventLoopUniqueID,
107
        parentGroup: MultiThreadedEventLoopGroup?,  // nil iff thread take-over
108
        canEventLoopBeShutdownIndividually: Bool,
109
        selectorFactory: @escaping (NIOThread) throws -> NIOPosix.Selector<NIORegistration>,
110
        initializer: @escaping ThreadInitializer,
111
        metricsDelegate: NIOEventLoopMetricsDelegate?,
112
        _ callback: @escaping (SelectableEventLoop) -> Void
113
0
    ) {
114
0
        assert(thread.isCurrentSlow)
115
0
        uniqueID.attachToCurrentThread()
116
0
        defer {
117
0
            uniqueID.detachFromCurrentThread()
118
0
        }
119
0
        initializer(thread)
120
0
121
0
        do {
122
0
            let loop = SelectableEventLoop(
123
0
                thread: thread,
124
0
                uniqueID: uniqueID,
125
0
                parentGroup: parentGroup,
126
0
                selector: try selectorFactory(thread),
127
0
                canBeShutdownIndividually: canEventLoopBeShutdownIndividually,
128
0
                metricsDelegate: metricsDelegate
129
0
            )
130
0
            Self.storeLoopThreadLocalReference(to: loop)
131
0
            defer {
132
0
                Self.clearLoopThreadLocalReference()
133
0
            }
134
0
            callback(loop)
135
0
            try loop.run()
136
0
        } catch {
137
0
            // We fatalError here because the only reasons this can be hit is if the underlying kqueue/epoll give us
138
0
            // errors that we cannot handle which is an unrecoverable error for us.
139
0
            fatalError("Unexpected error while running SelectableEventLoop: \(error).")
140
0
        }
141
0
    }
142
143
    private static func setupThreadAndEventLoop(
144
        name: String,
145
        uniqueID: SelectableEventLoopUniqueID,
146
        parentGroup: MultiThreadedEventLoopGroup,
147
        selectorFactory: @escaping (NIOThread) throws -> NIOPosix.Selector<NIORegistration>,
148
        initializer: @escaping ThreadInitializer,
149
        metricsDelegate: NIOEventLoopMetricsDelegate?
150
0
    ) -> SelectableEventLoop {
151
0
        let lock = ConditionLock(value: 0)
152
0
153
0
        // synchronised by `lock`
154
0
        var _loop: SelectableEventLoop! = nil
155
0
156
0
        NIOThread.spawnAndRun(name: name) { t in
157
0
            MultiThreadedEventLoopGroup.runTheLoop(
158
0
                thread: t,
159
0
                uniqueID: uniqueID,
160
0
                parentGroup: parentGroup,
161
0
                canEventLoopBeShutdownIndividually: false,  // part of MTELG
162
0
                selectorFactory: selectorFactory,
163
0
                initializer: initializer,
164
0
                metricsDelegate: metricsDelegate
165
0
            ) { l in
166
0
                lock.lock(whenValue: 0)
167
0
                _loop = l
168
0
                lock.unlock(withValue: 1)
169
0
            }
170
0
        }
171
0
        lock.lock(whenValue: 1)
172
0
        defer { lock.unlock() }
173
0
        return _loop!
174
0
    }
175
176
    /// Creates a `MultiThreadedEventLoopGroup` instance which uses `numberOfThreads`.
177
    ///
178
    /// - Note: Don't forget to call `shutdownGracefully` or `syncShutdownGracefully` when you no longer need this
179
    ///         `EventLoopGroup`. If you forget to shut the `EventLoopGroup` down you will leak `numberOfThreads`
180
    ///         (kernel) threads which are costly resources. This is especially important in unit tests where one
181
    ///         `MultiThreadedEventLoopGroup` is started per test case.
182
    ///
183
    /// - arguments:
184
    ///   - numberOfThreads: The number of `Threads` to use.
185
0
    public convenience init(numberOfThreads: Int) {
186
0
        self.init(
187
0
            numberOfThreads: numberOfThreads,
188
0
            canBeShutDown: .yes,
189
0
            metricsDelegate: nil,
190
0
            selectorFactory: NIOPosix.Selector<NIORegistration>.init
191
0
        )
192
0
    }
193
194
    /// Creates a `MultiThreadedEventLoopGroup` instance which uses `numberOfThreads`.
195
    ///
196
    /// - Note: Don't forget to call `shutdownGracefully` or `syncShutdownGracefully` when you no longer need this
197
    ///         `EventLoopGroup`. If you forget to shut the `EventLoopGroup` down you will leak `numberOfThreads`
198
    ///         (kernel) threads which are costly resources. This is especially important in unit tests where one
199
    ///         `MultiThreadedEventLoopGroup` is started per test case.
200
    ///
201
    /// - Parameters:
202
    ///    - numberOfThreads: The number of `Threads` to use.
203
    ///    - metricsDelegate: Delegate for collecting information from this eventloop
204
0
    public convenience init(numberOfThreads: Int, metricsDelegate: NIOEventLoopMetricsDelegate) {
205
0
        self.init(
206
0
            numberOfThreads: numberOfThreads,
207
0
            canBeShutDown: .yes,
208
0
            metricsDelegate: metricsDelegate,
209
0
            selectorFactory: NIOPosix.Selector<NIORegistration>.init
210
0
        )
211
0
    }
212
213
    /// Create a ``MultiThreadedEventLoopGroup`` that cannot be shut down and must not be `deinit`ed.
214
    ///
215
    /// This is only useful for global singletons.
216
    public static func _makePerpetualGroup(
217
        threadNamePrefix: String,
218
        numberOfThreads: Int
219
0
    ) -> MultiThreadedEventLoopGroup {
220
0
        self.init(
221
0
            numberOfThreads: numberOfThreads,
222
0
            canBeShutDown: .no,
223
0
            threadNamePrefix: threadNamePrefix,
224
0
            metricsDelegate: nil,
225
0
            selectorFactory: NIOPosix.Selector<NIORegistration>.init
226
0
        )
227
0
    }
228
229
    internal convenience init(
230
        numberOfThreads: Int,
231
        metricsDelegate: NIOEventLoopMetricsDelegate?,
232
        selectorFactory: @escaping (NIOThread) throws -> NIOPosix.Selector<NIORegistration>
233
0
    ) {
234
0
        precondition(numberOfThreads > 0, "numberOfThreads must be positive")
235
0
        let initializers: [ThreadInitializer] = Array(repeating: { _ in }, count: numberOfThreads)
236
0
        self.init(
237
0
            threadInitializers: initializers,
238
0
            canBeShutDown: .yes,
239
0
            metricsDelegate: metricsDelegate,
240
0
            selectorFactory: selectorFactory
241
0
        )
242
0
    }
243
244
    internal convenience init(
245
        numberOfThreads: Int,
246
        canBeShutDown: _CanBeShutDown,
247
        threadNamePrefix: String,
248
        metricsDelegate: NIOEventLoopMetricsDelegate?,
249
        selectorFactory: @escaping (NIOThread) throws -> NIOPosix.Selector<NIORegistration>
250
0
    ) {
251
0
        precondition(numberOfThreads > 0, "numberOfThreads must be positive")
252
0
        let initializers: [ThreadInitializer] = Array(repeating: { _ in }, count: numberOfThreads)
253
0
        self.init(
254
0
            threadInitializers: initializers,
255
0
            canBeShutDown: canBeShutDown,
256
0
            threadNamePrefix: threadNamePrefix,
257
0
            metricsDelegate: metricsDelegate,
258
0
            selectorFactory: selectorFactory
259
0
        )
260
0
    }
261
262
    internal convenience init(
263
        numberOfThreads: Int,
264
        canBeShutDown: _CanBeShutDown,
265
        metricsDelegate: NIOEventLoopMetricsDelegate?,
266
        selectorFactory: @escaping (NIOThread) throws -> NIOPosix.Selector<NIORegistration>
267
0
    ) {
268
0
        precondition(numberOfThreads > 0, "numberOfThreads must be positive")
269
0
        let initializers: [ThreadInitializer] = Array(repeating: { _ in }, count: numberOfThreads)
270
0
        self.init(
271
0
            threadInitializers: initializers,
272
0
            canBeShutDown: canBeShutDown,
273
0
            metricsDelegate: metricsDelegate,
274
0
            selectorFactory: selectorFactory
275
0
        )
276
0
    }
277
278
    internal convenience init(
279
        threadInitializers: [ThreadInitializer],
280
        metricsDelegate: NIOEventLoopMetricsDelegate?,
281
0
        selectorFactory: @escaping (NIOThread) throws -> NIOPosix.Selector<NIORegistration> = NIOPosix.Selector<
282
0
            NIORegistration
283
0
        >
284
0
        .init
285
0
    ) {
286
0
        self.init(
287
0
            threadInitializers: threadInitializers,
288
0
            canBeShutDown: .yes,
289
0
            metricsDelegate: metricsDelegate,
290
0
            selectorFactory: selectorFactory
291
0
        )
292
0
    }
293
294
    /// Creates a `MultiThreadedEventLoopGroup` instance which uses the given `ThreadInitializer`s. One `NIOThread` per `ThreadInitializer` is created and used.
295
    ///
296
    /// - arguments:
297
    ///   - threadInitializers: The `ThreadInitializer`s to use.
298
    internal init(
299
        threadInitializers: [ThreadInitializer],
300
        canBeShutDown: _CanBeShutDown,
301
        threadNamePrefix: String = "NIO-ELT-",
302
        metricsDelegate: NIOEventLoopMetricsDelegate?,
303
0
        selectorFactory: @escaping (NIOThread) throws -> Selector<NIORegistration> = Selector<NIORegistration>
304
0
            .init
305
0
    ) {
306
0
        self.threadNamePrefix = threadNamePrefix
307
0
        let firstLoopID = SelectableEventLoopUniqueID.makeNextGroup()
308
0
        self.myGroupID = firstLoopID.groupID
309
0
        self.canBeShutDown = canBeShutDown
310
0
        self.eventLoops = []  // Just so we're fully initialised and can vend `self` to the `SelectableEventLoop`.
311
0
        var loopUniqueID = firstLoopID
312
0
        self.eventLoops = threadInitializers.map { initializer in
313
0
            // Maximum name length on linux is 16 by default.
314
0
            let ev = MultiThreadedEventLoopGroup.setupThreadAndEventLoop(
315
0
                name: "\(threadNamePrefix)\(loopUniqueID.groupID)-#\(loopUniqueID.loopID)",
316
0
                uniqueID: loopUniqueID,
317
0
                parentGroup: self,
318
0
                selectorFactory: selectorFactory,
319
0
                initializer: initializer,
320
0
                metricsDelegate: metricsDelegate
321
0
            )
322
0
            loopUniqueID.nextLoop()
323
0
            return ev
324
0
        }
325
0
    }
326
327
0
    deinit {
328
0
        assert(
329
0
            self.canBeShutDown != .no,
330
0
            "Perpetual MTELG shut down, you must ensure that perpetual MTELGs don't deinit"
331
0
        )
332
0
    }
333
334
    /// Returns the `EventLoop` for the calling thread.
335
    ///
336
    /// - Returns: The current `EventLoop` for the calling thread or `nil` if none is assigned to the thread.
337
0
    public static var currentEventLoop: EventLoop? {
338
0
        self.currentSelectableEventLoop
339
0
    }
340
341
0
    internal static var currentSelectableEventLoop: SelectableEventLoop? {
342
0
        guard let ref = c_nio_posix_get_el_ptr() else {
343
0
            return nil
344
0
        }
345
0
346
0
        return Unmanaged<SelectableEventLoop>.fromOpaque(ref).takeUnretainedValue()
347
0
    }
348
349
    /// Returns an `EventLoopIterator` over the `EventLoop`s in this `MultiThreadedEventLoopGroup`.
350
    ///
351
    /// - Returns: `EventLoopIterator`
352
0
    public func makeIterator() -> EventLoopIterator {
353
0
        EventLoopIterator(self.eventLoops)
354
0
    }
355
356
    /// Returns the next `EventLoop` from this `MultiThreadedEventLoopGroup`.
357
    ///
358
    /// `MultiThreadedEventLoopGroup` uses _round robin_ across all its `EventLoop`s to select the next one.
359
    ///
360
    /// - Returns: The next `EventLoop` to use.
361
0
    public func next() -> EventLoop {
362
0
        self.nextSEL()
363
0
    }
364
365
0
    internal func nextSEL() -> SelectableEventLoop {
366
0
        eventLoops[abs(index.loadThenWrappingIncrement(ordering: .relaxed) % eventLoops.count)]
367
0
    }
368
369
    /// Returns the current `EventLoop` if we are on an `EventLoop` of this `MultiThreadedEventLoopGroup` instance.
370
    ///
371
    /// - Returns: The `EventLoop`.
372
0
    public func any() -> EventLoop {
373
0
        self.anySEL()
374
0
    }
375
376
0
    internal func anySEL() -> SelectableEventLoop {
377
0
        if let loop = Self.currentSelectableEventLoop,
378
0
            // We are on `loop`'s thread, so we may ask for the its parent group.
379
0
            loop.parentGroupCallableFromThisEventLoopOnly() === self
380
0
        {
381
0
            // Nice, we can return this.
382
0
            loop.assertInEventLoop()
383
0
            return loop
384
0
        } else {
385
0
            // Oh well, let's just vend the next one then.
386
0
            return self.nextSEL()
387
0
        }
388
0
    }
389
390
    /// Shut this `MultiThreadedEventLoopGroup` down which causes the `EventLoop`s and their associated threads to be
391
    /// shut down and release their resources.
392
    ///
393
    /// Even though calling `shutdownGracefully` more than once should be avoided, it is safe to do so and execution
394
    /// of the `handler` is guaranteed.
395
    ///
396
    /// - Parameters:
397
    ///    - queue: The `DispatchQueue` to run `handler` on when the shutdown operation completes.
398
    ///    - handler: The handler which is called after the shutdown operation completes. The parameter will be `nil`
399
    ///               on success and contain the `Error` otherwise.
400
    @preconcurrency
401
0
    public func shutdownGracefully(queue: DispatchQueue, _ handler: @escaping @Sendable (Error?) -> Void) {
402
0
        self._shutdownGracefully(queue: queue, handler)
403
0
    }
404
405
    internal func _shutdownGracefully(
406
        queue: DispatchQueue,
407
        allowShuttingDownOverride: Bool = false,
408
        _ handler: @escaping _ShutdownGracefullyCallback
409
0
    ) {
410
0
        switch self.canBeShutDown {
411
0
        case .yes:
412
0
            ()  // ok
413
0
        case .no:
414
0
            queue.async {
415
0
                handler(EventLoopError._unsupportedOperation)
416
0
            }
417
0
            return
418
0
        case .notByUser:
419
0
            guard allowShuttingDownOverride else {
420
0
                queue.async {
421
0
                    handler(EventLoopError._unsupportedOperation)
422
0
                }
423
0
                return
424
0
            }
425
0
        }
426
0
427
0
        // This method cannot perform its final cleanup using EventLoopFutures, because it requires that all
428
0
        // our event loops still be alive, and they may not be. Instead, we use Dispatch to manage
429
0
        // our shutdown signaling, and then do our cleanup once the DispatchQueue is empty.
430
0
        let g = DispatchGroup()
431
0
        let q = DispatchQueue(label: "nio.shutdownGracefullyQueue", target: queue)
432
0
        let wasRunning: Bool = self.shutdownLock.withLock {
433
0
            // We need to check the current `runState` and react accordingly.
434
0
            switch self.runState {
435
0
            case .running:
436
0
                // If we are still running, we set the `runState` to `closing`,
437
0
                // so that potential future invocations know, that the shutdown
438
0
                // has already been initiaited.
439
0
                self.runState = .closing([])
440
0
                return true
441
0
            case .closing(var callbacks):
442
0
                // If we are currently closing, we need to register the `handler`
443
0
                // for invocation after the shutdown is completed.
444
0
                callbacks.append((q, handler))
445
0
                self.runState = .closing(callbacks)
446
0
                return false
447
0
            case .closed(let error):
448
0
                // If we are already closed, we can directly dispatch the `handler`
449
0
                q.async {
450
0
                    handler(error)
451
0
                }
452
0
                return false
453
0
            }
454
0
        }
455
0
456
0
        // If the `runState` was not `running` when `shutdownGracefully` was called,
457
0
        // the shutdown has already been initiated and we have to return here.
458
0
        guard wasRunning else {
459
0
            return
460
0
        }
461
0
462
0
        let result: NIOLockedValueBox<Result<Void, Error>> = NIOLockedValueBox(.success(()))
463
0
464
0
        for loop in self.eventLoops {
465
0
            g.enter()
466
0
            loop.initiateClose(queue: q) { closeResult in
467
0
                switch closeResult {
468
0
                case .success:
469
0
                    ()
470
0
                case .failure(let error):
471
0
                    result.withLockedValue {
472
0
                        $0 = .failure(error)
473
0
                    }
474
0
                }
475
0
                g.leave()
476
0
            }
477
0
        }
478
0
479
0
        g.notify(queue: q) {
480
0
            for loop in self.eventLoops {
481
0
                loop.syncFinaliseClose(joinThread: true)
482
0
            }
483
0
            let (overallError, queueCallbackPairs): (Error?, [(DispatchQueue, _ShutdownGracefullyCallback)]) = self
484
0
                .shutdownLock.withLock {
485
0
                    switch self.runState {
486
0
                    case .closed, .running:
487
0
                        preconditionFailure(
488
0
                            "MultiThreadedEventLoopGroup in illegal state when closing: \(self.runState)"
489
0
                        )
490
0
                    case .closing(let callbacks):
491
0
                        let overallError: Error? = result.withLockedValue {
492
0
                            switch $0 {
493
0
                            case .success:
494
0
                                return nil
495
0
                            case .failure(let error):
496
0
                                return error
497
0
                            }
498
0
                        }
499
0
                        self.runState = .closed(overallError)
500
0
                        return (overallError, callbacks)
501
0
                    }
502
0
                }
503
0
504
0
            queue.async {
505
0
                handler(overallError)
506
0
            }
507
0
            for queueCallbackPair in queueCallbackPairs {
508
0
                queueCallbackPair.0.async {
509
0
                    queueCallbackPair.1(overallError)
510
0
                }
511
0
            }
512
0
        }
513
0
    }
514
515
    /// Convert the calling thread into an `EventLoop`.
516
    ///
517
    /// This function will not return until the `EventLoop` has stopped. You can initiate stopping the `EventLoop` by
518
    /// calling `eventLoop.shutdownGracefully` which will eventually make this function return.
519
    ///
520
    /// - Parameters:
521
    ///   - callback: Called _on_ the `EventLoop` that the calling thread was converted to, providing you the
522
    ///                 `EventLoop` reference. Just like usually on the `EventLoop`, do not block in `callback`.
523
0
    public static func withCurrentThreadAsEventLoop(_ callback: @escaping (EventLoop) -> Void) {
524
0
        NIOThread.withCurrentThread { callingThread in
525
0
            MultiThreadedEventLoopGroup.runTheLoop(
526
0
                thread: callingThread,
527
0
                uniqueID: .makeNextGroup(),
528
0
                parentGroup: nil,
529
0
                canEventLoopBeShutdownIndividually: true,
530
0
                selectorFactory: NIOPosix.Selector<NIORegistration>.init,
531
0
                initializer: { _ in },
532
0
                metricsDelegate: nil,
533
0
                { loop in
534
0
                    loop.assertInEventLoop()
535
0
                    callback(loop)
536
0
                }
537
0
            )
538
0
        }
539
0
    }
540
541
0
    public func _preconditionSafeToSyncShutdown(file: StaticString, line: UInt) {
542
0
        if let eventLoop = MultiThreadedEventLoopGroup.currentEventLoop {
543
0
            preconditionFailure(
544
0
                """
545
0
                BUG DETECTED: syncShutdownGracefully() must not be called when on an EventLoop.
546
0
                Calling syncShutdownGracefully() on any EventLoop can lead to deadlocks.
547
0
                Current eventLoop: \(eventLoop)
548
0
                """,
549
0
                file: file,
550
0
                line: line
551
0
            )
552
0
        }
553
0
    }
554
}
555
556
extension MultiThreadedEventLoopGroup: @unchecked Sendable {}
557
558
extension MultiThreadedEventLoopGroup: CustomStringConvertible {
559
0
    public var description: String {
560
0
        "MultiThreadedEventLoopGroup { threadPattern = \(self.threadNamePrefix)\(self.myGroupID)-#* }"
561
0
    }
562
}
563
564
@usableFromInline
565
struct ErasedUnownedJob: Sendable {
566
    @usableFromInline
567
    let erasedJob: any Sendable
568
569
    @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
570
0
    init(job: UnownedJob) {
571
0
        self.erasedJob = job
572
0
    }
573
574
    @available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
575
    @inlinable
576
0
    var unownedJob: UnownedJob {
577
0
        // This force-cast is safe since we only store an UnownedJob
578
0
        self.erasedJob as! UnownedJob
579
0
    }
580
}
581
582
@usableFromInline
583
internal struct ScheduledTask {
584
    @usableFromInline
585
    enum Kind {
586
        case task(task: () -> Void, failFn: (Error) -> Void)
587
        case callback(any NIOScheduledCallbackHandler)
588
    }
589
590
    @usableFromInline
591
    let kind: Kind
592
593
    /// The id of the scheduled task.
594
    ///
595
    /// - Important: This id has two purposes. First, it is used to give this struct an identity so that we can implement ``Equatable``
596
    ///     Second, it is used to give the tasks an order which we use to execute them.
597
    ///     This means, the ids need to be unique for a given ``SelectableEventLoop`` and they need to be in ascending order.
598
    @usableFromInline
599
    let id: UInt64
600
601
    @usableFromInline
602
    internal let readyTime: NIODeadline
603
604
    @usableFromInline
605
0
    init(id: UInt64, _ task: @escaping () -> Void, _ failFn: @escaping (Error) -> Void, _ time: NIODeadline) {
606
0
        self.id = id
607
0
        self.readyTime = time
608
0
        self.kind = .task(task: task, failFn: failFn)
609
0
    }
610
611
    @usableFromInline
612
0
    init(id: UInt64, _ handler: any NIOScheduledCallbackHandler, _ time: NIODeadline) {
613
0
        self.id = id
614
0
        self.readyTime = time
615
0
        self.kind = .callback(handler)
616
0
    }
617
}
618
619
extension ScheduledTask: CustomStringConvertible {
620
    @usableFromInline
621
0
    var description: String {
622
0
        "ScheduledTask(readyTime: \(self.readyTime))"
623
0
    }
624
}
625
626
extension ScheduledTask: Comparable {
627
    @usableFromInline
628
0
    static func < (lhs: ScheduledTask, rhs: ScheduledTask) -> Bool {
629
0
        if lhs.readyTime == rhs.readyTime {
630
0
            return lhs.id < rhs.id
631
0
        } else {
632
0
            return lhs.readyTime < rhs.readyTime
633
0
        }
634
0
    }
635
636
    @usableFromInline
637
0
    static func == (lhs: ScheduledTask, rhs: ScheduledTask) -> Bool {
638
0
        lhs.id == rhs.id
639
0
    }
640
}
641
642
@available(*, unavailable)
643
extension ScheduledTask: Sendable {}
644
645
@available(*, unavailable)
646
extension ScheduledTask.Kind: Sendable {}
647
648
extension NIODeadline {
649
    @inlinable
650
0
    func readyIn(_ target: NIODeadline) -> TimeAmount {
651
0
        if self < target {
652
0
            return .nanoseconds(0)
653
0
        }
654
0
        return self - target
655
0
    }
656
}
657
658
extension MultiThreadedEventLoopGroup {
659
    /// Start & automatically shut down a new ``MultiThreadedEventLoopGroup``.
660
    ///
661
    /// This method allows to start & automatically dispose of a ``MultiThreadedEventLoopGroup`` following the principle of Structured Concurrency.
662
    /// The ``MultiThreadedEventLoopGroup`` is guaranteed to be shut down upon return, whether `body` throws or not.
663
    ///
664
    /// - Note: Outside of top-level code (typically in your main function) or tests, you should generally not use this function to create a new
665
    ///         ``MultiThreadedEventLoopGroup`` because creating & destroying threads is expensive. Instead, share an existing one.
666
    @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
667
    public static func withEventLoopGroup<Result>(
668
        numberOfThreads: Int,
669
        metricsDelegate: (any NIOEventLoopMetricsDelegate)? = nil,
670
        isolation actor: isolated (any Actor)? = #isolation,
671
        _ body: (MultiThreadedEventLoopGroup) async throws -> Result
672
0
    ) async throws -> Result {
673
0
        let group = MultiThreadedEventLoopGroup(
674
0
            numberOfThreads: numberOfThreads,
675
0
            canBeShutDown: .notByUser,  // We want to prevent direct user shutdowns.
676
0
            metricsDelegate: metricsDelegate,
677
0
            selectorFactory: NIOPosix.Selector<NIORegistration>.init
678
0
        )
679
0
        return try await asyncDo {
680
0
            try await body(group)
681
0
        } finally: { _ in
682
0
            let q = DispatchQueue(label: "MTELG.shutdown")
683
0
            let _: () = try await withCheckedThrowingContinuation { (cont) -> Void in
684
0
                group._shutdownGracefully(queue: q, allowShuttingDownOverride: true) { error in
685
0
                    if let error {
686
0
                        cont.resume(throwing: error)
687
0
                    } else {
688
0
                        cont.resume()
689
0
                    }
690
0
                }
691
0
            }
692
0
        }
693
0
    }
694
}
695
#endif  // !os(WASI)