Skip to content

Attempt to improve test reliability#73

Merged
lawrence-forooghian merged 3 commits intomainfrom
2025-08-21-improve-test-reliability
Aug 22, 2025
Merged

Attempt to improve test reliability#73
lawrence-forooghian merged 3 commits intomainfrom
2025-08-21-improve-test-reliability

Conversation

@lawrence-forooghian
Copy link
Collaborator

@lawrence-forooghian lawrence-forooghian commented Aug 21, 2025

This attempts to address the intermittently failing and hanging CI tests by:

  1. fixing a couple of issues in the tests
  2. running the integration tests serially (hopefully a temporary measure, to be revisited in Further investigate flaky integration tests and restore concurrent execution #72)

See commit messages for more details.

(Note that CI is failing because of the repo rename causing the docs builds to fail; I'll probably merge this one anyway because the flaky tests are quite a nuisance, making it hard to get anything else merged.)

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of object cleanup and tombstone handling, reducing missed or duplicated completion notifications and making timing resilient to clock skew.
  • Performance
    • Faster, more responsive garbage collection with refined intervals and immediate completion signaling (no buffering), reducing overall wait times.
  • Tests
    • Serialized JS integration tests to reduce flakiness.
    • Deterministic ordering for map-based fixtures.
    • Updated helpers to wait based on tombstone time rather than fixed cycles, increasing test stability.

Mistake in 70306a0. Correct approach copied from "Objects.createCounter
sends COUNTER_CREATE operation" test.
Wait until we're sure that GC of an entry should have occurred,
regardless of clock skew. Also fix what may have been a bug in 792683d
where we may not wait for as many GC cycles as we intended to (because
of counting AsyncStream buffered values).
I've been observing a bunch of failures and hangs etc in these tests;
see the comment and the referenced issue. In order to unblock CI whilst
we prepare for a release, run the tests in serial, which seems to
improve reliability at the unfortunate cost of making them a _lot_
slower to run (from ~15s on my machine up to 4 minutes).
@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

Walkthrough

Introduces a no-buffering stream for garbage-collection completion in InternalDefaultRealtimeObjects and updates references. Test suite execution is serialized. JS integration tests switch from GC cycle-count waits to tombstone-time-based waits, refactor map creation to deterministic ordering, and use typed GC options with adjusted intervals.

Changes

Cohort / File(s) Summary
Realtime Objects GC Stream (no-buffering)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
Replaces buffered GC completion stream with a no-buffering variant (bufferingNewest(0)); updates initializations, continuations, and invocation wiring; renames test-facing property to testsOnly_completedGarbageCollectionEventsWithoutBuffering; adjusts comments and finish paths.
JS Integration Tests: serialization, tombstone-based waits, deterministic maps
Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
Adds .serialized to suite; replaces waitForGCCycles(Int) with waitForTombstonedObjectsToBeCollected(Date) async throws across contexts; switches GC timing to InternalDefaultRealtimeObjects.GarbageCollectionOptions with interval 0.5, gracePeriod 0.25; refactors map creation using indexed tuples for deterministic order; updates assertions to use tombstonedAt.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Test as JS Integration Test
  participant Obj as Realtime Objects
  participant GC as GC Scheduler
  participant Stream as GC Completion Stream (no-buffering)

  rect rgba(230,240,255,0.5)
  note over Test,Obj: Setup
  Test->>Obj: Configure GarbageCollectionOptions(interval=0.5, gracePeriod=0.25)
  end

  rect rgba(240,255,240,0.5)
  note over Test,Obj: Tombstone
  Test->>Obj: Tombstone object (record tombstonedAt)
  Obj->>GC: Schedule collection after gracePeriod
  end

  rect rgba(255,250,230,0.6)
  note over GC,Stream: Collection & Event
  GC-->>Obj: Collect tombstoned object
  Obj-->>Stream: Emit GC completion (no buffering)
  end

  rect rgba(255,240,245,0.6)
  note over Test: Await completion
  Test->>Test: waitForTombstonedObjectsToBeCollected(tombstonedAt)
  Test-->>Obj: Assert removal from pool/data
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble through code like clover at dawn,
Streams now slim, no buffers to yawn.
Tombstones whisper, “wait by the clock,”
GC tiptoes—tick, then knock.
Tests line up, single-file parade—
Maps in order, flake-free ballet played.
Thump-thump—ship it, unafraid! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 2025-08-21-improve-test-reliability

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@lawrence-forooghian lawrence-forooghian changed the title 2025 08 21 improve test reliability Attempt to improve test reliability Aug 21, 2025
@lawrence-forooghian lawrence-forooghian marked this pull request as ready for review August 21, 2025 21:23
@lawrence-forooghian
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

✅ Actions performed

Review triggered.

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift (2)

3893-3898: Typed GC options in tests improve clarity; minor stylistic nit.

Using InternalDefaultRealtimeObjects.GarbageCollectionOptions(interval: 0.5, gracePeriod: 0.25) is clear. If you want to follow the project guideline “prefer implicit .init when type is inferable,” you could inline the type on the variable and use .init:

-        let garbageCollectionOptions = InternalDefaultRealtimeObjects.GarbageCollectionOptions(
-            interval: 0.5,
-            gracePeriod: 0.25,
-        )
+        let garbageCollectionOptions: InternalDefaultRealtimeObjects.GarbageCollectionOptions = .init(
+            interval: 0.5,
+            gracePeriod: 0.25
+        )

3910-3920: Avoid capturing non-Sendable state in a @sendable closure by pre-capturing the stream.

The closure waitForTombstonedObjectsToBeCollected is declared @sendable. Capturing internallyTypedObjects (likely non-Sendable) can trigger strict-concurrency diagnostics. Pre-capture the AsyncStream (which is Sendable when Element is Sendable) and only close over that.

Apply this diff locally:

-            let internallyTypedObjects = try #require(objects as? PublicDefaultRealtimeObjects)
-            let waitForTombstonedObjectsToBeCollected: @Sendable (Date) async throws -> Void = { (tombstonedAt: Date) in
+            let internallyTypedObjects = try #require(objects as? PublicDefaultRealtimeObjects)
+            // Capture the stream outside the @Sendable closure to avoid capturing a non-Sendable object.
+            let gcEvents = internallyTypedObjects.testsOnly_proxied.testsOnly_completedGarbageCollectionEventsWithoutBuffering
+            let waitForTombstonedObjectsToBeCollected: @Sendable (Date) async throws -> Void = { (tombstonedAt: Date) in
                 // Sleep until we're sure we're past tombstonedAt + gracePeriod
                 let timeUntilGracePeriodExpires = (tombstonedAt + garbageCollectionOptions.gracePeriod).timeIntervalSince(.init())
                 if timeUntilGracePeriodExpires > 0 {
                     try await Task.sleep(nanoseconds: UInt64(timeUntilGracePeriodExpires * Double(NSEC_PER_SEC)))
                 }
 
-                // Wait for the next GC event
-                await internallyTypedObjects.testsOnly_proxied.testsOnly_completedGarbageCollectionEventsWithoutBuffering.first { _ in true }
+                // Wait for the next GC event
+                await gcEvents.first { _ in true }
             }

Optionally (not required), consider Task.sleep(for:) with Duration for readability and to avoid floating-point rounding:

try await Task.sleep(for: .seconds(timeUntilGracePeriodExpires))

This requires iOS 17/tvOS 17 deployment in tests (already present elsewhere in this file).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 19ba2e9 and 7df42d2.

📒 Files selected for processing (2)
  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (3 hunks)
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift (10 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/swift.mdc)

**/*.swift: When initializer type is inferable, prefer the implicit .init(...) form over repeating the type name
When enum type is inferable, prefer the shorthand .caseName over repeating the type name
For JSONValue or WireValue, prefer literal syntax enabled by ExpressibleBy*Literal conformances where possible
For JSON strings, prefer Swift raw string literals rather than escaping double quotes
When an array literal begins with an initializer expression, place the initializer on the line after the opening bracket

Files:

  • Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift
  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
**/Tests/**/*.swift

📄 CodeRabbit inference engine (.cursor/rules/testing.mdc)

**/Tests/**/*.swift: Use the Swift Testing framework (import Testing), not XCTest.
Do not use fatalError in response to a test expectation failure. Favour the usage of Swift Testing's #require macro.
Only add labels to test cases or suites when the label is different to the name of the suite struct or test method.
Follow the guidelines given under 'Attributing tests to a spec point' in the file CONTRIBUTING.md in order to tag the unit tests with the relevant specification points. Make sure to follow the exact format of the comments as described in that file. Pay particular attention to the difference between the meaning of @spec and @specPartial and be sure not to write @spec multiple times for the same specification point.
Add comments that explain when some piece of test data is not important for the scenario being tested.
When you need to import the following modules in the tests, do so in the following way: Ably: use import Ably; AblyLiveObjects: use @testable import AblyLiveObjects; AblyPlugin: use import AblyPlugin; do not do internal import.
When you need to pass a logger to internal components in the tests, pass TestLogger().
When you need to unwrap an optional value in the tests, favour using #require instead of guard let.
When creating testsOnly_ property declarations, do not write generic comments of the form 'Test-only access to the private createOperationIsMerged property'; the meaning of these properties is already well understood.

Files:

  • Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift
🔇 Additional comments (8)
Sources/AblyLiveObjects/Internal/InternalDefaultRealtimeObjects.swift (4)

101-101: Good call: no-backlog GC completion stream using bufferingNewest(0).

Using AsyncStream.makeStream(bufferingPolicy: .bufferingNewest(0)) is a precise fit for “only care about the next completion after a wait begins,” which helps avoid races with stale GC completions.


335-336: Plumbing the new continuation into performGarbageCollection is correct.

Routing eventsContinuation: completedGarbageCollectionEventsWithoutBufferingContinuation ensures tests wait for a fresh GC cycle rather than receiving buffered signals.


340-346: testsOnly_completedGarbageCollectionEventsWithoutBuffering accessor matches semantics.

The naming and visibility are clear for test-only usage. No issues.


355-361: Test helper now finishes the new GC stream — nice.

Finishing completedGarbageCollectionEventsWithoutBufferingContinuation avoids leaked tasks in tests that await the stream.

Tests/AblyLiveObjectsTests/JS Integration Tests/ObjectsIntegrationTests.swift (4)

337-345: Serialize the suite to deflake CI (temporary) — pragmatic.

Adding .serialized to the Suite is an acceptable short-term mitigation for the hangs/timeouts described in PR context (issue #72). Keep an eye on runtime impact and revert once the underlying issue is addressed.

If helpful, I can generate a small “timing snapshot” script to compare wall-clock duration before/after serialization across CI runs to quantify the impact.


2927-2944: Deterministic ordering for concurrent map creation — solid fix for flakiness.

Returning (index, map) from the task group, then sorting by index, removes nondeterminism from concurrent operations while preserving concurrency benefits.


3791-3795: Tombstone-time-based waits reduce flakiness compared to “N GC cycles.”

Deriving tombstonedAt and waiting for the first GC after gracePeriod has elapsed is a robust approach and matches the new no-buffering stream semantics.

Also applies to: 3867-3870


3741-3741: Context includes an async wait function — good seam for reuse.

Adding waitForTombstonedObjectsToBeCollected into the test Context keeps scenarios clean and reusable. This plays well with the no-buffering GC stream.

@lawrence-forooghian lawrence-forooghian merged commit 07a771e into main Aug 22, 2025
17 of 18 checks passed
@lawrence-forooghian lawrence-forooghian deleted the 2025-08-21-improve-test-reliability branch August 22, 2025 12:00
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