Skip to content

[AIT-276] feat: introduce ACK-based local application of LiveObjects ops#1194

Open
ttypic wants to merge 1 commit intomainfrom
AIT-276/apply-on-ack
Open

[AIT-276] feat: introduce ACK-based local application of LiveObjects ops#1194
ttypic wants to merge 1 commit intomainfrom
AIT-276/apply-on-ack

Conversation

@ttypic
Copy link
Contributor

@ttypic ttypic commented Feb 26, 2026

introduce ACK-based local application of LiveObjects ops

  • Implemented synthetic ACK-based object message application logic (publishAndApply).
  • Added buffering and order-preserving application of ACK results during sync (applyAckResult).
  • Introduced ObjectsOperationSource enum to distinguish operation sources (ACK vs. channel).
  • Updated applyObject and related object-specific management functions to utilize the source enum.
  • Enhanced tests for ACK-based application and updated handling of unsupported operations (returns false instead of throwing).

Summary by CodeRabbit

Release Notes

  • New Features

    • Added site code support for LiveObjects to identify server instances.
    • Enhanced local operation handling for LiveCounter and LiveMap with server acknowledgment confirmation.
  • Bug Fixes

    • Improved error handling for unsupported operations.

…operations

- Implemented synthetic ACK-based object message application logic (`publishAndApply`).
- Added buffering and order-preserving application of ACK results during sync (`applyAckResult`).
- Introduced `ObjectsOperationSource` enum to distinguish operation sources (ACK vs. channel).
- Updated `applyObject` and related object-specific management functions to utilize the source enum.
- Enhanced tests for ACK-based application and updated handling of unsupported operations (returns `false` instead of throwing).
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Walkthrough

This PR introduces a site-code-aware acknowledgment-based local operation system for LiveObjects. It adds siteCode propagation through connection details, implements a publishAndApply flow that applies operations locally upon ACK, introduces source-aware operation handling with deduplication, and enhances ACK buffering with ordered processing.

Changes

Cohort / File(s) Summary
Connection Infrastructure
lib/src/main/java/io/ably/lib/transport/ConnectionManager.java, lib/src/main/java/io/ably/lib/types/ConnectionDetails.java
Added siteCode field to propagate server instance identifiers from connection details and support site-based operation keying.
Error Handling
liveobjects/src/main/kotlin/io/ably/lib/objects/ErrorCodes.kt
Added new error code PublishAndApplyFailedDueToChannelState(92_008) for publish-and-apply channel state failures.
Operation Source Tracking
liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectsOperationSource.kt
Introduced new enum to distinguish between LOCAL (applied on ACK) and CHANNEL (received from Realtime) operation sources.
Publish-and-Apply Flow
liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt
Refactored publish mechanism: publish() now returns PublishResult, added publishAndApply() for local application after ACK, introduced appliedOnAckSerials deduplication tracking, updated create operations to use ACK-aware flow.
ACK Buffering & Deduplication
liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectsManager.kt
Added bufferedAcks mechanism with applyAckResult() and failBufferedAcks(), integrated source-aware operation processing, added appliedOnAckSerials deduplication to prevent duplicate application of ACK-applied operations.
Base Type Updates
liveobjects/src/main/kotlin/io/ably/lib/objects/type/BaseRealtimeObject.kt
Updated applyObject() and applyObjectOperation() signatures to accept ObjectsOperationSource parameter and return Boolean to indicate success; added source-conditional logic for siteTimeserials updates.
LiveCounter Implementation
liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt, liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterManager.kt
Replaced publish with publishAndApply for increment operations; updated applyOperation() to return Boolean and notify updates per operation; changed error handling from exceptions to false returns.
LiveMap Implementation
liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt, liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt
Replaced publish with publishAndApply for set/remove operations; updated applyOperation() to return Boolean with per-operation notifications; changed error handling from exceptions to false returns.
Test Updates
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsManagerTest.kt, liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/...Test.kt, liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/...Test.kt
Updated all test calls to applyObject/applyObjectMessages to pass ObjectsOperationSource parameter; changed exception-based assertions to boolean result checks for unsupported operations.

Sequence Diagram

sequenceDiagram
    participant Client
    participant DefaultRealtimeObjects
    participant ObjectsManager
    participant Channel
    participant Server

    Client->>DefaultRealtimeObjects: publishAndApply(operation)
    DefaultRealtimeObjects->>Channel: publish(message)
    DefaultRealtimeObjects->>Server: await ACK
    Server-->>DefaultRealtimeObjects: PublishResult (serial, siteCode)
    DefaultRealtimeObjects->>DefaultRealtimeObjects: validate serial & siteCode
    DefaultRealtimeObjects->>DefaultRealtimeObjects: create synthetic message
    DefaultRealtimeObjects->>ObjectsManager: applyObjectMessages(message, LOCAL)
    ObjectsManager->>ObjectsManager: applyAckResult(buffer deferred)
    ObjectsManager->>ObjectsManager: deduplicate via appliedOnAckSerials
    ObjectsManager->>Client: apply locally + complete deferred
    Note over DefaultRealtimeObjects,ObjectsManager: LOCAL source ensures<br/>siteTimeserials not updated<br/>CHANNEL source updates state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A site code hops in, ACKs dance with care,
LocalOps echo through the living air,
Dedup serials track what's been applied,
Boolean returns make truth clarified,
LiveObjects sing in perfect sync! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing ACK-based local application of LiveObjects operations, which is the core feature across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch AIT-276/apply-on-ack

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

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.

🧹 Nitpick comments (4)
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt (1)

102-105: Consider asserting the returned apply status as part of these scenarios.
Since applyObject() now returns Boolean, asserting it here would improve contract-level coverage.

Suggested test tightening
-    liveMap.applyObject(message, ObjectsOperationSource.CHANNEL)
+    val applied = liveMap.applyObject(message, ObjectsOperationSource.CHANNEL)
+    assertEquals(false, applied)

...
-    liveMap.applyObject(message, ObjectsOperationSource.CHANNEL)
+    val applied = liveMap.applyObject(message, ObjectsOperationSource.CHANNEL)
+    assertEquals(true, applied)

Also applies to: 132-135

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt`
around lines 102 - 105, The test should assert the Boolean return value from
liveMap.applyObject(...) to cover the contract; update DefaultLiveMapTest to
capture the result of applyObject(message, ObjectsOperationSource.CHANNEL) (and
the other applyObject call around lines 132-135) and add assertions like
assertFalse or assertTrue as appropriate based on the scenario before verifying
siteTimeserials, so the test checks both the returned apply status and the
side-effect on liveMap.siteTimeserials.
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt (1)

92-95: Consider asserting applyObject() boolean result in these tests.
These cases already validate side effects; also asserting the returned Boolean would protect the new apply contract from regressions.

Suggested test tightening
-    liveCounter.applyObject(message, ObjectsOperationSource.CHANNEL)
+    val applied = liveCounter.applyObject(message, ObjectsOperationSource.CHANNEL)
+    assertEquals(false, applied)

...
-    liveCounter.applyObject(message, ObjectsOperationSource.CHANNEL)
+    val applied = liveCounter.applyObject(message, ObjectsOperationSource.CHANNEL)
+    assertEquals(true, applied)

Also applies to: 119-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt`
around lines 92 - 95, The tests call
DefaultLiveCounterTest::liveCounter.applyObject(message,
ObjectsOperationSource.CHANNEL) but don’t assert its boolean return; capture the
result and assert it matches the expected outcome (e.g., assertFalse(result)
when the operation is skipped and the siteTimeserials remains "serial2",
assertTrue(result) when the operation should be applied). Update both
occurrences (the call around the siteTimeserials "serial2" check and the similar
block at lines 119-122) to assign the return value from
liveCounter.applyObject(...) to a variable and assert the Boolean equals the
expected applied/skipped state.
liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt (2)

319-330: Address swallowed exception warning from static analysis.

The try-catch on line 323 catches and swallows exceptions when accessing adapter.getChannel(channelName).reason. While the exception is used as an optional cause for the created AblyException, if an unexpected exception occurs (not just the channel not being available), it's silently ignored.

Consider being more specific about what exceptions to catch, or log the swallowed exception:

🔧 Proposed fix to log swallowed exception
-          val errorReason = try { adapter.getChannel(channelName).reason } catch (e: Exception) { null }
+          val errorReason = try {
+            adapter.getChannel(channelName).reason
+          } catch (e: Exception) {
+            Log.d(tag, "Could not retrieve channel reason during state change handling", e)
+            null
+          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt`
around lines 319 - 330, The try-catch around
adapter.getChannel(channelName).reason swallows any Exception, hiding unexpected
errors; update the code in DefaultRealtimeObjects (around adapter.getChannel,
channelName, errorReason, ablyException, objectsManager.failBufferedAcks) to
avoid silent swallowing by either catching only the expected exception type(s)
thrown when a channel is absent or by logging the caught exception before
returning null; ensure the catch block records the exception (e.g., logger.warn
or logger.error with the exception) so unexpected failures are visible while
still allowing errorReason to be null for building the AblyException.

212-221: Early returns silently degrade to echo-based application.

When siteCode is null or serials is unavailable/wrong length, the function logs an error and returns early, causing operations to be applied only when echoed from the server. This is a reasonable fallback, but consider whether callers should be aware of this degraded behavior.

If this degradation is intentional and acceptable (operations still eventually apply), the current approach is fine. If callers need to know about the failure to apply locally, consider returning a result indicating the outcome.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt`
around lines 212 - 221, The current early-return behavior when
adapter.connectionManager.siteCode is null or publishResult.serials is
null/length-mismatch (logged via Log.e with tag) silently degrades to echo-based
application; change the function to surface this outcome to callers instead of
only logging and returning—either by returning a status/result (e.g., boolean or
enum) indicating "appliedLocally", "degradedToEcho", or an error, or by invoking
the existing callback/throwing a descriptive exception; update all references to
siteCode, publishResult.serials, and objectMessages handling so callers can
react to the degraded path rather than being unaware.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt`:
- Around line 319-330: The try-catch around
adapter.getChannel(channelName).reason swallows any Exception, hiding unexpected
errors; update the code in DefaultRealtimeObjects (around adapter.getChannel,
channelName, errorReason, ablyException, objectsManager.failBufferedAcks) to
avoid silent swallowing by either catching only the expected exception type(s)
thrown when a channel is absent or by logging the caught exception before
returning null; ensure the catch block records the exception (e.g., logger.warn
or logger.error with the exception) so unexpected failures are visible while
still allowing errorReason to be null for building the AblyException.
- Around line 212-221: The current early-return behavior when
adapter.connectionManager.siteCode is null or publishResult.serials is
null/length-mismatch (logged via Log.e with tag) silently degrades to echo-based
application; change the function to surface this outcome to callers instead of
only logging and returning—either by returning a status/result (e.g., boolean or
enum) indicating "appliedLocally", "degradedToEcho", or an error, or by invoking
the existing callback/throwing a descriptive exception; update all references to
siteCode, publishResult.serials, and objectMessages handling so callers can
react to the degraded path rather than being unaware.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt`:
- Around line 92-95: The tests call
DefaultLiveCounterTest::liveCounter.applyObject(message,
ObjectsOperationSource.CHANNEL) but don’t assert its boolean return; capture the
result and assert it matches the expected outcome (e.g., assertFalse(result)
when the operation is skipped and the siteTimeserials remains "serial2",
assertTrue(result) when the operation should be applied). Update both
occurrences (the call around the siteTimeserials "serial2" check and the similar
block at lines 119-122) to assign the return value from
liveCounter.applyObject(...) to a variable and assert the Boolean equals the
expected applied/skipped state.

In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt`:
- Around line 102-105: The test should assert the Boolean return value from
liveMap.applyObject(...) to cover the contract; update DefaultLiveMapTest to
capture the result of applyObject(message, ObjectsOperationSource.CHANNEL) (and
the other applyObject call around lines 132-135) and add assertions like
assertFalse or assertTrue as appropriate based on the scenario before verifying
siteTimeserials, so the test checks both the returned apply status and the
side-effect on liveMap.siteTimeserials.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3298bcc and fa4d3f9.

📒 Files selected for processing (16)
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • lib/src/main/java/io/ably/lib/types/ConnectionDetails.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/ErrorCodes.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectsManager.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectsOperationSource.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/type/BaseRealtimeObject.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/DefaultLiveCounter.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/LiveCounterManager.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/DefaultLiveMap.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/LiveMapManager.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsManagerTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/DefaultLiveCounterTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livecounter/LiveCounterManagerTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/DefaultLiveMapTest.kt
  • liveobjects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt

@ttypic ttypic requested a review from sacOO7 February 27, 2026 11:59
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.

1 participant