Skip to content

Fix races in test helpers that made tests flaky - #1379

Merged
sacOO7 merged 2 commits into
integration/v2from
test/stabilise-flaky-test-helpers
Nov 2, 2026
Merged

sacOO7 merged 2 commits into
integration/v2from
test/stabilise-flaky-test-helpers

Conversation

@sacOO7

@sacOO7 sacOO7 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Intermittent CI failures came from races in tests and test helpers, not from the SDK. Test-only changes; no product code and no CHANGELOG entry.

Fix

  • ConnectionAwaiter (used by WaitForState, about 300 call sites): it checked the connection state before subscribing, so a state change landing between the two was missed and the wait timed out with "Expected 'Connected' but current state was 'Connected'" (seen in DeltaSpecs.WhenMessageReceived_WithDeltaError_ShouldNotPassMessageToChannelSubscriber). It now subscribes first, then checks, removes its listener on every exit path, uses TrySetResult, runs continuations asynchronously and cancels its timeout delay. New ConnectionAwaiterTests cover the already-there, reached, timeout and listener-removal cases.
  • LocalDeviceTests.WhenClientIdChangesAfterRegisteringDevice_...: the second GotPushDeviceDetails is raised on a background task as soon as the first HandleEvent releases its lock, and the test registered ProcessingEventCallback only after that call, so it could miss it (CI: "Expected (taskAwaiter) to be true, but found False"). The callback is now registered first and signals on the second event.
  • Push tests sharing LocalDevice.Instance: the process-wide static was reset and read by parallel test classes. LocalDeviceTests, the ActivationStateMachineTests, PushChannelTests and PushRealtimeTests classes now share one non-parallel xUnit collection.
  • TaskCompletionAwaiter.Tick: the decrement is now atomic (Interlocked.Decrement), so two handlers ticking at once can't lose a tick.
  • PresenceSandboxSpecs.Presence_ShouldHaveInternalMapForCurrentConnectionId: each step now waits on each channel for the specific expected event (action and clientId) instead of taking the first presence event of any kind, and a timeout names the channel and event that is missing. An extra valid event, such as a PRESENT from a sync (seen once in CI), is now ignored and only listed in the timeout message. One assertion that could never fail (msgB.Should().BeNull() with no handler subscribed) was removed; all others are kept.
  • UTS StateWaitTests.ConnectionStaysFailedWhenTheServerClosesAfterAFatalError: UtsRecord.ConnectionStates only records changes made after it subscribes. The test created an AutoConnect client and then subscribed, and the mock answers with a fatal ERROR at once, so on a slow runner the client could reach FAILED (and stay there) first, and the poll for FAILED timed out (CI: "StateWaitTests.cs:109: poll_until timed out after 10000 ms"; reproduced locally under CPU load). The client is now created with AutoConnect = false, the recorder attached, then Connect() called; the test still checks the same race across 20 clients. Two RealtimeUnitSmokeTests and MockWebSocketTests.EveryMockEventTypeIsEmittedInOrder had the same subscribe-after-connect pattern and get the same change.

The UTS RTL6c2_QueuedWhenDisconnected race is not in this PR; it belongs with the UTS tests in #1362.

The branch is based on the current integration/v2 and has two commits: the test-helper race fixes (including the review fixes), then the UTS StateWaitTests fix.

Tests

Race evidence, using throwaway harnesses that are not committed:

Race Injected delay Before After
ConnectionAwaiter check-then-subscribe 100 ms between the state check and the subscribe, CONNECTED sent during the gap 29/30 and 40/40 waits timed out 0/30 and 0/40
LocalDeviceTests callback registration Task.Delay(300) after the first HandleEvent 5/5 failed 5/5 passed

Without an injected delay the windows are too narrow to hit on demand; the new ConnectionAwaiterTests concurrency test passes but would not reliably catch a regression on its own.

Repeated runs (net6.0):

Run integration/v2 This PR
LocalDeviceTests ×20 20/20 20/20
LocalDeviceTests ×20 under CPU stress 19/20 (the CI failure) 20/20
Push test classes + ConnectionAwaiterTests ×10 – 10/10
Sandbox presence test 5/5 15/15 (5 + 10)

Validation

On 4f770114 (the pushed head):

  • ./build.sh --target=Build.NetStandard: build succeeded, 0 warnings, 0 errors. The .NET Framework test project also builds (0/0).
  • ./build.sh --target=Test.NetStandard.Unit.WithRetry --framework=net6.0: 1511 total, 1492 passed, 19 skipped, 0 failed.
  • An independent review re-ran the race evidence and the repeated runs above, both gates (same totals), and checked the diff for scope, handler leaks and kept assertions: pass.

On 54047a83 (the same content before the branch was rebuilt on the current integration/v2):

  • ./build.sh --target=Build.NetStandard: 0 warnings, 0 errors.
  • ./build.sh --target=Test.NetStandard.Uts --framework=net6.0 --uts-tier=unit: 1094 total, 856 passed, 238 skipped, 0 failed.
  • StateWaitTests: before the fix, 1 of 30 runs failed under CPU load with the CI error; after it, 30 of 30 passed under the same load, and the three changed infra test classes passed 30 of 30 runs.
  • ./build.sh --target=Test.NetStandard.Unit.WithRetry --framework=net6.0: 1556 total, 0 failed after retry (one timing-based ConnectionFailureSpecs backoff test failed once and passed on retry; not touched here).

On ac7a319f (the two commits on integration/v2 0feb7bd2): the content is identical to 54047a83. git diff against integration/v2 is byte-identical to the earlier one, and the trees differ only in the nuspec <authors> lines that integration/v2 now carries. An independent review confirmed the equivalence, the two-commit history and the commit messages: pass. CI runs the tests on this head.

Summary by CodeRabbit

  • Bug Fixes
    • Improved connection-state waiting so transitions are less likely to be missed, timeouts are handled reliably, and listeners are cleaned up.
    • Made task completion counting safer when multiple operations complete concurrently.
    • Updated push and presence tests to wait for the expected events more reliably and avoid interference from unrelated events.
  • Tests
    • Grouped push tests that share state to prevent them from running in parallel.

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 31 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d249a0e9-59ad-420b-b298-137120db3284
Commits

Reviewing files that changed from the base of the PR and between 4f77011 and ac7a319.

Files selected for processing: 13
  • src/Ably.PubSub.Tests.Shared/Ably.PubSub.Tests.Shared.projitems
  • src/Ably.PubSub.Tests.Shared/Infrastructure/ConnectionAwaiter.cs
  • src/Ably.PubSub.Tests.Shared/Infrastructure/ConnectionAwaiterTests.cs
  • src/Ably.PubSub.Tests.Shared/Infrastructure/TaskCompletionAwaiter.cs
  • src/Ably.PubSub.Tests.Shared/Push/ActivationStateMachineTests.cs
  • src/Ably.PubSub.Tests.Shared/Push/LocalDeviceTests.cs
  • src/Ably.PubSub.Tests.Shared/Push/PushChannelTests.cs
  • src/Ably.PubSub.Tests.Shared/Push/PushRealtimeTests.cs
  • src/Ably.PubSub.Tests.Shared/Push/PushTestCollection.cs
  • src/Ably.PubSub.Tests.Shared/Realtime/PresenceSandboxSpecs.cs
  • src/Ably.PubSub.Tests.Shared/Uts/InfraTests/StateWaitTests.cs
  • src/Ably.PubSub.Tests.Shared/Uts/InfraTests/Unit/MockWebSocketTests.cs
  • src/Ably.PubSub.Tests.Shared/Uts/InfraTests/Unit/RealtimeUnitSmokeTests.cs
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b30154da-69d4-46e4-81ba-2fcb68c8d81e
Commits

Reviewing files that changed from the base of the PR and between fd4de79 and 4f77011.

Files selected for processing: 10
  • src/Ably.PubSub.Tests.Shared/Ably.PubSub.Tests.Shared.projitems
  • src/Ably.PubSub.Tests.Shared/Infrastructure/ConnectionAwaiter.cs
  • src/Ably.PubSub.Tests.Shared/Infrastructure/ConnectionAwaiterTests.cs
  • src/Ably.PubSub.Tests.Shared/Infrastructure/TaskCompletionAwaiter.cs
  • src/Ably.PubSub.Tests.Shared/Push/ActivationStateMachineTests.cs
  • src/Ably.PubSub.Tests.Shared/Push/LocalDeviceTests.cs
  • src/Ably.PubSub.Tests.Shared/Push/PushChannelTests.cs
  • src/Ably.PubSub.Tests.Shared/Push/PushRealtimeTests.cs
  • src/Ably.PubSub.Tests.Shared/Push/PushTestCollection.cs
  • src/Ably.PubSub.Tests.Shared/Realtime/PresenceSandboxSpecs.cs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

The pull request updates shared test synchronization for connection and task completion, groups push tests that share LocalDevice.Instance, adjusts a push event test, and makes a presence sandbox test wait for events with matching actions and client IDs.

Changes

Asynchronous Test Waiters

Layer / File(s) Summary
Connection and task completion waiters
src/Ably.PubSub.Tests.Shared/Infrastructure/ConnectionAwaiter.cs, src/Ably.PubSub.Tests.Shared/Infrastructure/TaskCompletionAwaiter.cs, src/Ably.PubSub.Tests.Shared/Infrastructure/ConnectionAwaiterTests.cs, src/Ably.PubSub.Tests.Shared/Ably.PubSub.Tests.Shared.projitems
ConnectionAwaiter subscribes before checking the current state, removes its listener after waiting, and handles completion near timeout. TaskCompletionAwaiter uses atomic task-count updates. New tests cover completion, timeout, listener cleanup, and a state-change race.

Push Test Synchronization

Layer / File(s) Summary
Push collection and LocalDevice event test
src/Ably.PubSub.Tests.Shared/Push/PushTestCollection.cs, src/Ably.PubSub.Tests.Shared/Push/ActivationStateMachineTests.cs, src/Ably.PubSub.Tests.Shared/Push/LocalDeviceTests.cs, src/Ably.PubSub.Tests.Shared/Push/PushChannelTests.cs, src/Ably.PubSub.Tests.Shared/Push/PushRealtimeTests.cs, src/Ably.PubSub.Tests.Shared/Ably.PubSub.Tests.Shared.projitems
Push test classes are assigned to a collection with parallelization disabled. The LocalDeviceTests callback is registered before the first event and signals its waiter on the second event.

Presence Sandbox Test Waiters

Layer / File(s) Summary
Match and await presence events
src/Ably.PubSub.Tests.Shared/Realtime/PresenceSandboxSpecs.cs
The test waits for presence events matching the expected action and client ID. The helper reports pending expectations after a timeout and disposes its waiters.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Other

Possibly related PRs

  • ably/ably-pubsub-dotnet#1364: Both changes address missed connection-state transitions, though this pull request updates the test-only ConnectionAwaiter.
  • ably/ably-pubsub-dotnet#1371: Both changes address flaky Push.LocalDeviceTests behavior through event registration and test serialization.

Suggested reviewers: ttypic

Merge Risk: Low · previous review 4f770

This test-only change fixes several flaky-test races. One remaining gap is that PushRealtimeTests leaves a shared LocalDevice instance behind, which can make a later push test fail depending on execution order. Resetting the instance in that class's teardown closes it, and no production behavior is affected.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main changes: fixing race conditions in test helpers and tests that caused flaky test behavior.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 8 functions across 9 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

A rabbit waits for states to turn,
While task counts tick and listeners learn.
Push tests queue in one calm place,
Presence events match action and face.
Then every waiter hops away.

@sacOO7

sacOO7 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Approval is blocked by an unresolved compile error in PushTestCollection and a resource-leaking test that leaves 200 realtime clients and workflows undisposed.

2 open issues
What changed in this PR

This test-only pull request hardens asynchronous test helpers and push/presence tests against event-ordering races, missed callbacks, leaked listeners, and shared-state interference. No SDK production code is changed.

Changes:

  • Makes ConnectionAwaiter subscribe safely, clean up listeners, handle timeout races, and adds focused tests.
  • Serializes tests sharing LocalDevice.Instance and fixes callback ordering in LocalDeviceTests.
  • Makes task counting atomic and replaces broad presence-event waits with targeted event matching.
  • Addresses the following review findings before approval:
    • Use a valid collection-name reference in PushTestCollection.cs so the project compiles.
    • Dispose the realtime clients created by the concurrency test.
    • Reset LocalDevice.Instance during PushRealtimeTests teardown.
File Summary
src/​Ably.PubSub.Tests.Shared/​Realtime/​PresenceSandboxSpecs.cs Adds targeted presence-event waiters and improved timeout diagnostics.
src/​Ably.PubSub.Tests.Shared/​Push/​PushTestCollection.cs Defines non-parallel execution for tests sharing LocalDevice.Instance; the collection-name reference currently requires correction for compilation.
src/​Ably.PubSub.Tests.Shared/​Push/​PushRealtimeTests.cs Assigns realtime push tests to the shared collection; teardown should reset the process-wide LocalDevice.Instance.
src/​Ably.PubSub.Tests.Shared/​Push/​PushChannelTests.cs Assigns push channel tests to the shared push collection.
src/​Ably.PubSub.Tests.Shared/​Push/​LocalDeviceTests.cs Registers the activation callback before triggering the first event.
src/​Ably.PubSub.Tests.Shared/​Push/​ActivationStateMachineTests.cs Assigns activation state-machine tests to the shared push collection.
src/​Ably.PubSub.Tests.Shared/​Infrastructure/​TaskCompletionAwaiter.cs Uses an atomic decrement for concurrent ticks.
src/​Ably.PubSub.Tests.Shared/​Infrastructure/​ConnectionAwaiterTests.cs Adds coverage for connection-wait scenarios; the concurrency test should dispose each created realtime client.
src/​Ably.PubSub.Tests.Shared/​Infrastructure/​ConnectionAwaiter.cs Fixes state-wait subscription ordering and cleanup behavior.
src/​Ably.PubSub.Tests.Shared/​Ably.PubSub.Tests.Shared.projitems Includes the new awaiter tests and push collection definition.

Review effort: Lite

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  • Fix comments: Fix CodeRabbit comments on this PR

Test-only changes; no product code.

- ConnectionAwaiter (behind WaitForState) checked the connection state
  before subscribing, so a change landing between the two was missed
  and the wait timed out. It now subscribes first, then checks, removes
  its listener on every exit path, uses TrySetResult, runs continuations
  asynchronously and cancels its timeout delay. New ConnectionAwaiterTests
  cover the already-there, reached, timeout and listener-removal cases;
  the concurrency test disposes the client each of its 200 iterations
  creates.
- LocalDeviceTests registered ProcessingEventCallback after the call
  that raises the second GotPushDeviceDetails on a background task, so
  it could miss it. The callback is now registered first.
- Test classes that read or reset the process-wide LocalDevice.Instance
  (LocalDeviceTests, the ActivationStateMachineTests classes,
  PushChannelTests and PushRealtimeTests) now share one non-parallel
  xUnit collection, and PushRealtimeTests clears LocalDevice.Instance
  after its base teardown, as the others do.
- TaskCompletionAwaiter.Tick decrements atomically, so two concurrent
  ticks can't lose one.
- PresenceSandboxSpecs.Presence_ShouldHaveInternalMapForCurrentConnectionId
  waits on each channel for the specific expected presence event instead
  of the first event of any kind, and names the missing one on timeout.
  An assertion that could never fail is removed.
UtsRecord.ConnectionStates and ConnectionStateChanges only see changes
made after they subscribe. ConnectionStaysFailedWhenTheServerClosesAfterAFatalError
created an AutoConnect client and subscribed afterwards, and its mock
answers the first attempt with a fatal ERROR at once. On a slow runner
the client could reach FAILED, and stay there, before the recorder
subscribed, so the poll for FAILED timed out after 10 seconds. The
failure reproduced locally under CPU load.

Create the client with AutoConnect off, subscribe, then connect. The
test still checks the same race across 20 clients. Two smoke tests in
RealtimeUnitSmokeTests, and MockWebSocketTests' event-order test (which
subscribed after triggering a disconnect whose retry is refused at
once), had the same pattern and could lose the DISCONNECTED or first
CONNECTING change they read, so they now subscribe first too.
@sacOO7
sacOO7 force-pushed the test/stabilise-flaky-test-helpers branch from 54047a8 to ac7a319 Compare November 2, 2026 17:54
@sacOO7

sacOO7 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

PR 1379: follow-up comment on the review-reply SHAs (draft, not posted)

The branch has been rebuilt as two commits on the current integration/v2, with the same content,
so the commits cited in the review replies are no longer on it. Both review fixes are now part of
213b572 ("Fix races in test helpers that made tests flaky"):

  • 9401ab1 (dispose the clients in the ConnectionAwaiter concurrency test) → 213b572
  • 2fb7b0a (clear LocalDevice.Instance after PushRealtimeTests) → 213b572

The UTS StateWaitTests fix is the second commit, ac7a319.

@sacOO7
sacOO7 merged commit 3d64a68 into integration/v2 Nov 2, 2026
21 checks passed
@sacOO7
sacOO7 deleted the test/stabilise-flaky-test-helpers branch November 2, 2026 18:20

This branch was successfully deployed

1 active deployment
staging/pull/1379/features — ac7a319f Deployed Nov 2, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants