/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) |