Skip to content

Commit aa0f489

Browse files
committed
test: Add tests for MultiThreadedEventLoopGroup in NIOAsyncRuntime. The vast majority of these tests were ported from NIOPosix tests for its own MultiThreadedEventLoopGroup. These tests ensure basic feature parity between the two different implementations.
1 parent 040dfae commit aa0f489

1 file changed

Lines changed: 209 additions & 0 deletions

File tree

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Copyright (c) 2025 PassiveLogic, Inc.
4+
// Licensed under Apache License v2.0
5+
//
6+
// See LICENSE.txt for license information
7+
//
8+
// SPDX-License-Identifier: Apache-2.0
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import NIOConcurrencyHelpers
13+
import Testing
14+
15+
@testable import NIOAsyncRuntime
16+
@testable import NIOCore
17+
18+
// NOTE: These tests are copied and adapted from NIOPosixTests.EventLoopTest
19+
// They have been modified to use async running, among other things.
20+
21+
@Suite("MultiThreadedEventLoopGroupTests", .serialized, .timeLimit(.minutes(1)))
22+
final class MultiThreadedEventLoopGroupTests {
23+
@Test
24+
func testLotsOfMixedImmediateAndScheduledTasks() async throws {
25+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
26+
defer {
27+
#expect(throws: Never.self) {
28+
try group.syncShutdownGracefully()
29+
}
30+
}
31+
32+
let eventLoop = group.next()
33+
struct Counter: Sendable {
34+
private var _submitCount = NIOLockedValueBox(0)
35+
var submitCount: Int {
36+
get { self._submitCount.withLockedValue { $0 } }
37+
nonmutating set { self._submitCount.withLockedValue { $0 = newValue } }
38+
}
39+
private var _scheduleCount = NIOLockedValueBox(0)
40+
var scheduleCount: Int {
41+
get { self._scheduleCount.withLockedValue { $0 } }
42+
nonmutating set { self._scheduleCount.withLockedValue { $0 = newValue } }
43+
}
44+
}
45+
46+
let achieved = Counter()
47+
var immediateTasks = [EventLoopFuture<Void>]()
48+
var scheduledTasks = [Scheduled<Void>]()
49+
for _ in (0..<100_000) {
50+
if Bool.random() {
51+
let task = eventLoop.submit {
52+
achieved.submitCount += 1
53+
}
54+
immediateTasks.append(task)
55+
}
56+
if Bool.random() {
57+
let task = eventLoop.scheduleTask(in: .microseconds(10)) {
58+
achieved.scheduleCount += 1
59+
}
60+
scheduledTasks.append(task)
61+
}
62+
}
63+
64+
let submitCount = try await EventLoopFuture.whenAllSucceed(immediateTasks, on: eventLoop).map({
65+
_ in
66+
achieved.submitCount
67+
}).get()
68+
#expect(submitCount == achieved.submitCount)
69+
70+
let scheduleCount = try await EventLoopFuture.whenAllSucceed(
71+
scheduledTasks.map { $0.futureResult },
72+
on: eventLoop
73+
)
74+
.map({ _ in
75+
achieved.scheduleCount
76+
}).get()
77+
#expect(scheduleCount == scheduledTasks.count)
78+
}
79+
80+
@Test
81+
func testLotsOfMixedImmediateAndScheduledTasksFromEventLoop() async throws {
82+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
83+
defer {
84+
#expect(throws: Never.self) {
85+
try group.syncShutdownGracefully()
86+
}
87+
}
88+
89+
let eventLoop = group.next()
90+
struct Counter: Sendable {
91+
private var _submitCount = NIOLockedValueBox(0)
92+
var submitCount: Int {
93+
get { self._submitCount.withLockedValue { $0 } }
94+
nonmutating set { self._submitCount.withLockedValue { $0 = newValue } }
95+
}
96+
private var _scheduleCount = NIOLockedValueBox(0)
97+
var scheduleCount: Int {
98+
get { self._scheduleCount.withLockedValue { $0 } }
99+
nonmutating set { self._scheduleCount.withLockedValue { $0 = newValue } }
100+
}
101+
}
102+
103+
let achieved = Counter()
104+
let (immediateTasks, scheduledTasks) = try await eventLoop.submit {
105+
var immediateTasks = [EventLoopFuture<Void>]()
106+
var scheduledTasks = [Scheduled<Void>]()
107+
for _ in (0..<100_000) {
108+
if Bool.random() {
109+
let task = eventLoop.submit {
110+
achieved.submitCount += 1
111+
}
112+
immediateTasks.append(task)
113+
}
114+
if Bool.random() {
115+
let task = eventLoop.scheduleTask(in: .microseconds(10)) {
116+
achieved.scheduleCount += 1
117+
}
118+
scheduledTasks.append(task)
119+
}
120+
}
121+
return (immediateTasks, scheduledTasks)
122+
}.get()
123+
124+
let submitCount = try await EventLoopFuture.whenAllSucceed(immediateTasks, on: eventLoop)
125+
.map({ _ in
126+
achieved.submitCount
127+
}).get()
128+
#expect(submitCount == achieved.submitCount)
129+
130+
let scheduleCount = try await EventLoopFuture.whenAllSucceed(
131+
scheduledTasks.map { $0.futureResult },
132+
on: eventLoop
133+
)
134+
.map({ _ in
135+
achieved.scheduleCount
136+
}).get()
137+
#expect(scheduleCount == scheduledTasks.count)
138+
}
139+
140+
@Test
141+
func testImmediateTasksDontGetStuck() async throws {
142+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
143+
defer {
144+
#expect(throws: Never.self) {
145+
try group.syncShutdownGracefully()
146+
}
147+
}
148+
149+
let eventLoop = group.next()
150+
let testEventLoop = MultiThreadedEventLoopGroup.singleton.any()
151+
152+
let longWait = TimeAmount.seconds(60)
153+
let failDeadline = NIODeadline.now() + longWait
154+
let (immediateTasks, scheduledTask) = try await eventLoop.submit {
155+
// Submit over the 4096 immediate tasks, and some scheduled tasks
156+
// with expiry deadline in (nearish) future.
157+
// We want to make sure immediate tasks, even those that don't fit
158+
// in the first batch, don't get stuck waiting for scheduled task
159+
// expiry
160+
let immediateTasks = (0..<5000).map { _ in
161+
eventLoop.submit {}.hop(to: testEventLoop)
162+
}
163+
let scheduledTask = eventLoop.scheduleTask(in: longWait) {
164+
}
165+
166+
return (immediateTasks, scheduledTask)
167+
}.get()
168+
169+
// The immediate tasks should all succeed ~immediately.
170+
// We're testing for a case where the EventLoop gets confused
171+
// into waiting for the scheduled task expiry to complete
172+
// some immediate tasks.
173+
_ = try await EventLoopFuture.whenAllSucceed(immediateTasks, on: testEventLoop).get()
174+
#expect(.now() < failDeadline)
175+
176+
scheduledTask.cancel()
177+
}
178+
179+
@Test
180+
func testInEventLoopABAProblem() async throws {
181+
// Older SwiftNIO versions had a bug here, they held onto `pthread_t`s for ever (which is illegal) and then
182+
// used `pthread_equal(pthread_self(), myPthread)`. `pthread_equal` just compares the pointer values which
183+
// means there's an ABA problem here. This test checks that we don't suffer from that issue now.
184+
let allELs: NIOLockedValueBox<[any EventLoop]> = NIOLockedValueBox([])
185+
186+
for _ in 0..<100 {
187+
let group = MultiThreadedEventLoopGroup(numberOfThreads: 4)
188+
defer {
189+
#expect(throws: Never.self) {
190+
try group.syncShutdownGracefully()
191+
}
192+
}
193+
for loop in group.makeIterator() {
194+
try! await loop.submit {
195+
allELs.withLockedValue { allELs in
196+
#expect(loop.inEventLoop)
197+
for otherEL in allELs {
198+
#expect(
199+
!otherEL.inEventLoop,
200+
"should only be in \(loop) but turns out also in \(otherEL)"
201+
)
202+
}
203+
allELs.append(loop)
204+
}
205+
}.get()
206+
}
207+
}
208+
}
209+
}

0 commit comments

Comments
 (0)