Skip to content

test: flakiness improvements - #1124

Merged
pblazej merged 7 commits into
mainfrom
blaze/yet-flaky
Sep 14, 2026
Merged

pblazej merged 7 commits into
mainfrom
blaze/yet-flaky

Conversation

@pblazej

@pblazej pblazej commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes the causes behind the 30-minute Build & Test timeouts of Sep 10–13 — a flushEvents() barrier race in DropOldestContinuationTests, an uncancellable UniFFI read under next(within:), RemoteParticipantTests faking inactivity by wiping participant info, and, found via the new stall-dump watchdog, an AsyncCompleter deadlock between its timeout and cancellation paths — one commit each, with retries dropped so a flake fails red instead of doubling the run.

pblazej and others added 3 commits September 14, 2026 10:21
flushEvents() only orders events already yielded to the drain's stream; the
unstructured Task in sendAsync could submit after the evict/fail event it was
meant to settle, parking the write behind a fake channel that never drains and
hanging the test process. Signal after submit() returns so the barrier holds.

CI: 15 of 37 idle-until-timeout Build & Test jobs (Sep 10-13) ended on
evictionResolvesTheDisplacedWaiter / teardownFailsQueuedWaiters. Reproduced by
delaying the task 20 ms; the same delay passes with the barrier.

The file crosses swiftlint's 400-line limit; disabled as in
PeerConnectionSignalingTests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The UniFFI future under DataTrackStream.next() cannot be cancelled, so the task
group in next(within:) waited on it past the deadline and a single lost frame
hung the job. Race the read against an AsyncCompleter timeout and let a
timed-out read finish when the stream ends.

CI: 11 of 37 idle-until-timeout jobs (publishAndReceive largeFrames,
setPipelineOptionsReassemblesMultiPacketFrames, publishWithFrameMetadata).
Expecting one more frame than sent now fails in 15 s instead of hanging.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `...Timeout` tests faked an inactive participant with
set(info: .init(), connectionState:), which ignores the connection state and
instead blanks sid/identity and flips state to JOINING - failing a completer
keyed "" with participantRemoved, so the tests passed on an existing error
rather than a timeout, and leaving Room with participants it can no longer
address. Reset the room's active completer for the identity instead, so
waitUntilActive genuinely times out, and assert .timedOut.

CI: 4 of 37 idle-until-timeout jobs ended in waitUntillAnyActiveTimeout - the
only test that blanked both remote participants - after the expected throws and
before the leave reached the server. Does not reproduce on macOS (0/8 locally,
0/4 in CI), so this removes what was unique to the test rather than a verified
wedge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@pblazej pblazej changed the title Fix three test-process wedges behind the 30-minute CI timeouts Fix flaky tests Sep 14, 2026
@pblazej pblazej changed the title Fix flaky tests test Sep 14, 2026
@pblazej pblazej changed the title test test: flakiness improvements Sep 14, 2026
pblazej and others added 2 commits September 14, 2026 11:32
A hung Build & Test job leaves only silence: the client log stops, the server
sees pings and no leave, and the 30-minute timeout kills the host before
anything records where it was. Start a watchdog next to xcodebuild that, after
five minutes without log growth, writes for every xctest a `sample` (thread
stacks) and `swift-inspect dump-concurrency` (the runtime's task tree with async
backtraces), up to three times, and ship them with the test-log artifact.

Both are needed: a suspended task has no thread, so `sample` cannot show it,
while the task dump names it (validated locally on a leaked continuation and on
a blocked semaphore under xcodebuild; lldb's tasks plugin only unwinds tasks
that are on a thread). swift-inspect attaches to a test host without sudo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With Swift Testing, every retry iteration re-runs the whole selection, not the
failed test: one flake in iteration 1 (798 s) triggered a full second pass
(781 s) and the job ran into its 30-minute limit before the ObjC bundle could
finish. A flake now fails the job with its log attached instead of being
hidden behind a retry or turned into a timeout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Devin Review

quiet=$((quiet + 10))
[ "$quiet" -lt "$stall" ] && continue
quiet=0; dumps=$((dumps + 1)); mkdir -p "$out"
pids=$(pgrep -x xctest); [ -z "$pids" ] && pids=$(pgrep -x xcodebuild)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Simulator hangs omit test-host dumps

When simulator tests stall, pgrep -x xctest falls back to xcodebuild. Simulator bundles run inside app test hosts, so dumps omit the hung process.

Learn more

Simulator tests use a runner application process rather than the command-line xctest executable. Exact process names depend on the test bundle, while this workflow selects several bundles and platforms. Falling back to xcodebuild captures the orchestrator instead of the process executing the stalled test.

Example: An iOS LiveKitCoreTests case hangs inside its runner app. No process is named exactly xctest, so only xcodebuild is sampled and the blocked test stack is absent.

Recommended fix: Discover test-host descendants of the active xcodebuild process, including simulator runner applications and macOS test hosts. Dump each matching descendant rather than relying on the executable name.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

pblazej and others added 2 commits September 14, 2026 12:41
The timeout block and the cancellation handler both took `_lock` and resumed
the continuation inside it. Resuming needs the task's status-record lock, and
`swift_task_cancel` runs the cancellation handler while holding that lock - so
a waiter cancelled at the instant its own timer fired deadlocked two threads:
the timer queue holding `_lock`, the cancelling task holding the status record.
`waitUntilAnyActive` triggers exactly that when every child times out together;
the first throw cancels the siblings as their timers go off.

Caught by the stall-dump watchdog on CI (macos-15: three identical dumps five
minutes apart, `WaitEntry.timeout -> swift_continuation_throwingResume ->
waitForStatusRecordUnlock` against `swift_task_cancel -> onCancel -> _lock`) and
reproduced locally 3/3 with two waiters timing out under `prefix(1)`. Take the
entries out under the lock and settle them after it. The regression test races
2,000 such groups and is bounded by a completer of its own, so a recurrence
fails the test instead of wedging the job.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Moves cancelRacingTimeoutSettles next to the other completer tests and races
for a three-second budget instead of a fixed 2,000 iterations. On loaded
simulators the utility-QoS timers behind 4,000 one-millisecond waits fired
tens of seconds late (the 30 s bound's own timer fired ~25 s late on visionOS),
so slowness was indistinguishable from the deadlock the test guards against.
The pre-fix deadlock hits within the first 75 races, so a time box loses no
sensitivity, and only a deadlock can now leave the loop unfinished; the bound
is widened accordingly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pblazej

pblazej commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Let's revisit above failures after merging #1116 which seems to be related.

@pblazej
pblazej merged commit caf6a7b into main Sep 14, 2026
26 of 32 checks passed
@pblazej
pblazej deleted the blaze/yet-flaky branch September 14, 2026 11:49
pblazej added a commit that referenced this pull request Sep 15, 2026
Only conflict was adjacency in CompleterTests: `resumeRacingRegistrationNeverStrands`
(this branch) and `cancelRacingTimeoutSettles` (#1124) landed next to each other and
are independent — both kept.

The merge git did not flag: #1124 moved every `AsyncCompleter` resume outside `_lock`,
while this branch's re-check resumes inside it. Those auto-merge silently into the
lock-order inversion #1124 removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pblazej added a commit that referenced this pull request Sep 15, 2026
#1124 moved every resume in this type outside `_lock`: resuming a continuation needs
the task's status-record lock, and cancellation takes that lock before running
`onCancel`, which takes `_lock`. Resuming under `_lock` inverts the order and wedges.

The re-check added here resumed inside the lock, and merging main auto-merged the two
without a conflict -- leaving `timeout` and `onCancel` outside the lock and this one
inside it. Decide under the lock, carry the result out, resume after.

The existing test did not cover the window it was written for: one waiter against one
resume either has not started (and takes the cached result on the fast path) or has
already registered, so removing the re-check entirely still passed. Fan out to 32
waiters per round so a single resume can land inside one of them -- that fails in
under a second without the re-check. `resumeRacingCancellationSettles` covers the
inversion itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pblazej added a commit that referenced this pull request Sep 15, 2026
`_publishDataTest` fires 100 concurrent `publish(data:)` with a default-constructed
`DataPublishOptions`, whose `reliable` defaults to false, then asserts every topic
arrives. A lossy channel does not retransmit, and under `.dropOldest` a starved drain
evicts queued writes -- which is why this timed out in 4 of the last 5 Build & Test
runs on #1124 while passing locally. Same call this PR already made for
`PeerConnectionSignalingTests.dataChannel`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pblazej added a commit that referenced this pull request Sep 16, 2026
Fixes CocoaPods Lint, red on every run since #1124 replaced
`self._entries` with an implicit `_entries` — legal in the Swift 6
language mode SPM builds with, rejected by the Swift 5 mode the podspec
pins via `swift_versions = ["5.9"]`.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants