Skip to content

Fix NioAsyncWriter test on concurrency thread pool with single thread #3135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

orobio
Copy link
Contributor

@orobio orobio commented Mar 7, 2025

Motivation:

The testSuspendingBufferedYield_whenWriterFinished test fails on the Android emulator. See also the discussion in #3044.

Modifications:

The test requires at least two threads in the concurrency thread pool because it blocks one task, which waits for another task to set a condition. This PR adds support for running a task executor based on a NIOThreadPool and uses it for the test. Using a custom task executor guarantees that at least two threads are available for the test.

Additionally, the test has been renamed to testWriterFinish_AndSuspendBufferedYield, which is more in line with the other test names.

Result:

The test will pass regardless of the width of the global concurrency thread pool.

@orobio
Copy link
Contributor Author

orobio commented Mar 7, 2025

@finagolfin : Could you verify this fix on the Android emulator?

@Lukasa : I wanted to get some feedback first before writing tests for withNIOThreadPoolTaskExecutor. I will start creating some tests for that function if the fix works, and if this solution is deemed acceptable.

@finagolfin
Copy link
Contributor

Could you verify this fix on the Android emulator?

I can confirm this pull fixes the failing test on the Android emulator, 😃 once I updated the NIO package manifest to add this dependency. Ignore the failing trunk build: a new snapshot was just tagged which requires a three-hour build on my CI, so I manually canceled that trunk build.

The 6.0/6.1 linux runs passed with the emulator set to a single core, meaning this pull fixed that, whereas the mac runs now show other tests failing but this test passes. I don't know what the other mac test failures are about, but I've been seeing a lot of mac flakiness in the last couple hours, so I'm hoping that's a mac CI issue that will go away. 😉

@orobio orobio force-pushed the fix-NIOAsyncWriter-test-on-concurrency-thread-pool-with-single-thread branch from 3b535ec to 4957b3b Compare March 11, 2025 08:26
@orobio
Copy link
Contributor Author

orobio commented Mar 11, 2025

@finagolfin : That is good news. Thank you for verifying! I've added the dependency in Package.swift.

@Lukasa : Would this solution be acceptable? Using NIOThreadPool seemed like the obvious choice, and I designed it to ensure everything is cleaned up by the end of the test. However, incorrect usage could cause withNIOThreadPoolTaskExecutor to wait indefinitely for tasks to finish. I think guaranteed clean-up is a good thing to have for this, but let me know if you'd prefer a different approach.

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in general I'd accept this for this use-case, yeah. Let's just tweak a few things.

@orobio orobio force-pushed the fix-NIOAsyncWriter-test-on-concurrency-thread-pool-with-single-thread branch from 4957b3b to 66ec6ba Compare March 23, 2025 17:50
orobio added 4 commits March 23, 2025 18:59
The testSuspendingBufferedYield_whenWriterFinished test causes the test
application to hang in the Android emulator. This fix sets a timeout on
waiting for the ConditionLock, so we don't wait indefinitely.

Additionally, the timeout for waiting for both yields being suspended is
increased to make it less likely that an incorrect state is reached.
Provides withNIOThreadPoolTaskExecutor, which runs a task executor based
on a NIOThreadPool with a specified number of threads.
…ndroid emulator

The testSuspendingBufferedYield_whenWriterFinished test requires at least
two threads in the concurrency thread pool because it blocks one task,
which waits for another task to set a condition. In environments where
the global concurrency thread pool doesn’t have at least two threads
available, the test will fail, as observed on the Android emulator running
with a single virtual core (see discussion in apple#3044).

Using a custom task executor guarantees that at least two threads are
available for the test, regardless of the width of the global concurrency
thread pool.
@orobio orobio force-pushed the fix-NIOAsyncWriter-test-on-concurrency-thread-pool-with-single-thread branch from 66ec6ba to 7af7145 Compare March 23, 2025 17:59
Copy link
Contributor

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@glbrntt glbrntt enabled auto-merge (squash) March 27, 2025 14:27
@glbrntt glbrntt added the 🆕 semver/minor Adds new public API. label Mar 27, 2025
auto-merge was automatically disabled April 11, 2025 11:29

Head branch was pushed to by a user without write access

@orobio orobio force-pushed the fix-NIOAsyncWriter-test-on-concurrency-thread-pool-with-single-thread branch from c58b62e to 8afbc3f Compare April 11, 2025 11:41
Copy link
Member

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Just two minor comments

Comment on lines 134 to 161
/// Enqueue a job.
///
/// Called by the concurrency runtime.
///
/// - Parameter job: The job to enqueue.
public func enqueue(_ job: UnownedJob) {
self.storage.withLock { storage in
if storage.isShutdown {
fatalError("A job is enqueued after manual executor shutdown")
}
storage.jobs.append(job)
}
}

/// Shutdown.
///
/// Since the manual task executor is not running anything in the background, this is purely to catch
/// any issues due to incorrect usage of the executor. The shutdown verifies that the queue is empty
/// and makes sure that no new jobs can be enqueued.
@usableFromInline
func shutdown() {
self.storage.withLock { storage in
if !storage.jobs.isEmpty {
fatalError("Shutdown of manual executor with jobs in queue")
}
storage.isShutdown = true
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we really need this complicated shutdown handling. Since task executors can only be set on a scope we shouldn't have to do any manual clean up or reject jobs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can catch some incorrect usage, like for example:

await withManualTaskExecutor { taskExecutor in
    Task(executorPreference: taskExecutor) {
        try await Task.sleep(for: .seconds(3))
    }
    taskExecutor.runUntilQueueIsEmpty()  // Runs until the sleep starts, but the task is not finished yet
}

It will definitely not catch all problems, but I was thinking there could be value in catching some.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it is not a public thing anymore I'm fine with leaving it. @Lukasa @glbrntt you want to take a look before we merge it?

Copy link
Contributor

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this LGTM. Thanks @orobio!

@Lukasa Lukasa enabled auto-merge (squash) April 28, 2025 12:35
@Lukasa
Copy link
Contributor

Lukasa commented Apr 28, 2025

Looks like a few CI failures need cleaning up.

auto-merge was automatically disabled April 29, 2025 18:40

Head branch was pushed to by a user without write access

@orobio
Copy link
Contributor Author

orobio commented Apr 29, 2025

Looks like a few CI failures need cleaning up.

Should be fixed now, except for this one, which doesn't seem related:
https://github.com/apple/swift-nio/actions/runs/14705288968/job/41272725117?pr=3135

@orobio
Copy link
Contributor Author

orobio commented May 12, 2025

@Lukasa : Would we need to do anything for the third issue? Or can we rerun the CI?

@glbrntt glbrntt enabled auto-merge (squash) May 12, 2025 10:07
@glbrntt glbrntt disabled auto-merge May 12, 2025 10:07
@glbrntt glbrntt enabled auto-merge (squash) May 12, 2025 10:07
@glbrntt
Copy link
Contributor

glbrntt commented May 12, 2025

@orobio -- apologies for missing this one. I've just kicked the CI and enabled auto-merge. The last issue you linked above is a known issue which shouldn't block the merge.

@orobio
Copy link
Contributor Author

orobio commented May 12, 2025

It looks like another issue popped up, but I'm not sure what to do with that one either.

@glbrntt
Copy link
Contributor

glbrntt commented May 12, 2025

Looks like an infra problem, I've re-run that job.

@glbrntt glbrntt disabled auto-merge May 12, 2025 14:12
@glbrntt
Copy link
Contributor

glbrntt commented May 12, 2025

Something is up with that runner unrelated to your PR so I'm going to merge over the failure.

@glbrntt glbrntt merged commit a0c542b into apple:main May 12, 2025
39 of 41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants