Skip to content

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

Open
ttypic wants to merge 2 commits intomainfrom
AIT-276/apply-on-ack
Open

[AIT-276] feat: introduce ACK-based local application of LiveObjects ops#1194
ttypic wants to merge 2 commits 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

  • New Features

    • LiveObjects now propagates a site identifier for server instances.
    • Publish-and-apply flow: local changes are applied on server ACKs with improved deduplication and ordering between local ACKs and channel updates.
  • Bug Fixes

    • Better handling when channel state changes during publish, surfacing failures appropriately.
    • Unsupported operation paths now fail gracefully instead of causing crashes.

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Walkthrough

Adds propagation of a connection siteCode, implements a publish-and-apply ACK flow that applies local LiveObjects operations on server ACK, introduces source-aware operation handling and ACK buffering/deduplication, and updates many object managers and tests to use Boolean success returns and operation-source semantics.

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 ConnectionManager and ConnectionDetails; siteCode is deserialized from connection details and propagated on connect.
Publish & Local-Apply Flow
liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt
publish() now returns PublishResult; added publishAndApply() to wait for ACK and synthesize inbound messages for local apply; added appliedOnAckSerials for deduplication.
ACK Buffering & Ordering
liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectsManager.kt, liveobjects/src/main/kotlin/io/ably/lib/objects/TestHelpers.kt
Added buffered ACKs with applyAckResult() / failBufferedAcks(); endSync applies buffered LOCAL ACKs before CHANNEL messages; tests expose bufferedAcks getter.
Operation Source & Signatures
liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectsOperationSource.kt, liveobjects/src/main/kotlin/io/ably/lib/objects/type/BaseRealtimeObject.kt
New ObjectsOperationSource enum; applyObject/applyObjectOperation signatures now accept a source and return Boolean to indicate success.
LiveMap & LiveCounter updates
liveobjects/src/main/kotlin/io/ably/lib/objects/type/livemap/..., liveobjects/src/main/kotlin/io/ably/lib/objects/type/livecounter/...
Replaced publish with publishAndApply for create/set/inc/remove flows; applyOperation implementations now return Boolean, notify updates per-branch, and return false (not throw) on unsupported ops.
ErrorCode
liveobjects/src/main/kotlin/io/ably/lib/objects/ErrorCodes.kt
Added PublishAndApplyFailedDueToChannelState(92_008) error code for channel-state publish-and-apply failures.
Tests
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/...
Updated many tests to pass ObjectsOperationSource where applicable, adapted expectations to Boolean returns, added tests for LOCAL vs CHANNEL behavior, ACK deduplication, buffering, and publish-and-apply flows.

Sequence Diagram

sequenceDiagram
    participant Client
    participant DefaultRealtimeObjects
    participant Channel
    participant Server
    participant ObjectsManager

    Client->>DefaultRealtimeObjects: publishAndApply(op)
    DefaultRealtimeObjects->>Channel: publish(message)
    Channel->>Server: send
    Server-->>DefaultRealtimeObjects: ACK / PublishResult(serial, siteCode)
    DefaultRealtimeObjects->>DefaultRealtimeObjects: validate serial & siteCode
    DefaultRealtimeObjects->>DefaultRealtimeObjects: synthesize inbound ObjectMessage (LOCAL)
    DefaultRealtimeObjects->>ObjectsManager: applyObjectMessages([msg], LOCAL)
    ObjectsManager->>ObjectsManager: buffer/apply ACK results, dedupe via appliedOnAckSerials
    ObjectsManager-->>Client: complete deferred / return applied result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A tiny hop, a siteCode shared,

ACKs arrive and echoes cared,
Serials tracked to skip repeat,
LOCAL applies make state complete,
LiveObjects hum with rhythm sweet.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.11% 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 summarizes the main change: introducing ACK-based local application of LiveObjects operations, which aligns with the extensive changes throughout the codebase for implementing this feature.

✏️ 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

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.

@github-actions github-actions bot temporarily deployed to staging/pull/1194/features February 26, 2026 11:56 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1194/javadoc February 26, 2026 11:58 Inactive
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
@github-actions github-actions bot temporarily deployed to staging/pull/1194/features March 2, 2026 12:09 Inactive
@github-actions github-actions bot temporarily deployed to staging/pull/1194/javadoc March 2, 2026 12:10 Inactive
ttypic added 2 commits March 3, 2026 09:24
…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).
- Improved handling of skipped operations on ACK (RTO9a3) with detailed comments on echo discard logic.
- Added extensive unit tests for `ObjectsManager`, `LiveMapManager`, and `DefaultLiveCounter` covering edge cases like buffering, tombstoning, and operation deduplication.
- Introduced helper properties for buffered ACKs and object operations in tests.
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: 2

🧹 Nitpick comments (1)
liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsManagerTest.kt (1)

165-176: Assert the expected ObjectsOperationSource explicitly in these verifications.

Using any() here can miss regressions where ObjectsManager forwards the wrong source.

💡 Proposed test tightening
     verify(exactly = 1) {
-      testObject1.applyObject(objectMessage1, any())
+      testObject1.applyObject(objectMessage1, ObjectsOperationSource.CHANNEL)
     }
@@
     verify(exactly = 1) {
-      testObject2.applyObject(objectMessage2, any())
+      testObject2.applyObject(objectMessage2, ObjectsOperationSource.CHANNEL)
     }
@@
     verify(exactly = 1) {
-      testObject3.applyObject(objectMessage3, any())
+      testObject3.applyObject(objectMessage3, ObjectsOperationSource.CHANNEL)
     }
🤖 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/objects/ObjectsManagerTest.kt`
around lines 165 - 176, The verifications for applyObject use any() for the
source which can hide regressions; update each verify call on
testObject1.applyObject, testObject2.applyObject, and testObject3.applyObject to
assert the specific ObjectsOperationSource expected (e.g.,
OBJECTS_OPERATION_SOURCE_SYNC or the concrete enum/value used in your code)
instead of any(), so the test explicitly checks that ObjectsManager forwards the
correct source when syncing objects from the pool.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@liveobjects/src/main/kotlin/io/ably/lib/objects/DefaultRealtimeObjects.kt`:
- Around line 213-221: The code in DefaultRealtimeObjects.kt currently silently
returns when siteCode or publishResult.serials are null (the block checking
siteCode and serials), which causes downstream createMapAsync/createCounterAsync
to fail later; instead, change the behavior to fail fast or wait for echo: when
siteCode == null or serials == null/size mismatch, throw a clear exception (e.g.
IllegalStateException) or return a failed Future/CompletableDeferred so the
caller (createMapAsync/createCounterAsync) receives an explicit error; include
context (siteCode/publishResult info and objectMessages size) in the error
message so callers can handle retry/blocking for echo rather than proceeding
silently.

In `@liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectsManager.kt`:
- Around line 231-236: The ACK-echo dedup block that checks objectMessage.serial
against realtimeObjects.appliedOnAckSerials must only run for channel echoes;
update the conditional to also require the message source be a channel (e.g.
objectMessage.source == MessageSource.CHANNEL or the appropriate enum/constant
used in this codebase) before logging, removing the serial, and continuing, so
LOCAL replays do not clear dedup markers; keep the existing serial check and
remove/continue behavior but nest or extend the if to include the source check
(referencing objectMessage.serial, objectMessage.source, and
realtimeObjects.appliedOnAckSerials).

---

Nitpick comments:
In
`@liveobjects/src/test/kotlin/io/ably/lib/objects/unit/objects/ObjectsManagerTest.kt`:
- Around line 165-176: The verifications for applyObject use any() for the
source which can hide regressions; update each verify call on
testObject1.applyObject, testObject2.applyObject, and testObject3.applyObject to
assert the specific ObjectsOperationSource expected (e.g.,
OBJECTS_OPERATION_SOURCE_SYNC or the concrete enum/value used in your code)
instead of any(), so the test explicitly checks that ObjectsManager forwards the
correct source when syncing objects from the pool.

ℹ️ 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 fa4d3f9 and f57632f.

📒 Files selected for processing (17)
  • 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/TestHelpers.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
🚧 Files skipped from review as they are similar to previous changes (4)
  • lib/src/main/java/io/ably/lib/transport/ConnectionManager.java
  • liveobjects/src/main/kotlin/io/ably/lib/objects/ErrorCodes.kt
  • liveobjects/src/main/kotlin/io/ably/lib/objects/ObjectsOperationSource.kt
  • lib/src/main/java/io/ably/lib/types/ConnectionDetails.java

Comment on lines +213 to +221
if (siteCode == null) {
Log.e(tag, "RTO20c1: siteCode not available; operations will be applied when echoed")
return
}
val serials = publishResult.serials
if (serials == null || serials.size != objectMessages.size) {
Log.e(tag, "RTO20c2: PublishResult.serials unavailable or wrong length; operations will be applied when echoed")
return
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not silently return when ACK metadata is missing.

If siteCode/serials is unavailable, this path returns without local apply; downstream createMapAsync/createCounterAsync can then fail with “not applied as expected” even when publish succeeded. Fail explicitly (or block until echo) instead of silent fallback.

💡 Proposed fix
     val siteCode = adapter.connectionManager.siteCode
     if (siteCode == null) {
-      Log.e(tag, "RTO20c1: siteCode not available; operations will be applied when echoed")
-      return
+      throw serverError("RTO20c1: siteCode not available; cannot apply ACK locally")
     }
     val serials = publishResult.serials
     if (serials == null || serials.size != objectMessages.size) {
-      Log.e(tag, "RTO20c2: PublishResult.serials unavailable or wrong length; operations will be applied when echoed")
-      return
+      throw serverError("RTO20c2: PublishResult.serials unavailable or wrong length; cannot apply ACK locally")
     }
🤖 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 213 - 221, The code in DefaultRealtimeObjects.kt currently silently
returns when siteCode or publishResult.serials are null (the block checking
siteCode and serials), which causes downstream createMapAsync/createCounterAsync
to fail later; instead, change the behavior to fail fast or wait for echo: when
siteCode == null or serials == null/size mismatch, throw a clear exception (e.g.
IllegalStateException) or return a failed Future/CompletableDeferred so the
caller (createMapAsync/createCounterAsync) receives an explicit error; include
context (siteCode/publishResult info and objectMessages size) in the error
message so callers can handle retry/blocking for echo rather than proceeding
silently.

Comment on lines +231 to +236
if (objectMessage.serial != null &&
realtimeObjects.appliedOnAckSerials.contains(objectMessage.serial)) {
Log.d(tag, "RTO9a3: serial ${objectMessage.serial} already applied on ACK; discarding echo")
realtimeObjects.appliedOnAckSerials.remove(objectMessage.serial)
continue // discard without taking any further action
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restrict ACK-echo dedup logic to CHANNEL source.

This block currently runs for all sources. It should only run for channel echoes; otherwise LOCAL replays can clear dedup markers and allow a later echo to re-apply.

💡 Proposed fix
-      if (objectMessage.serial != null &&
-        realtimeObjects.appliedOnAckSerials.contains(objectMessage.serial)) {
+      if (source == ObjectsOperationSource.CHANNEL &&
+        objectMessage.serial != null &&
+        realtimeObjects.appliedOnAckSerials.remove(objectMessage.serial)
+      ) {
         Log.d(tag, "RTO9a3: serial ${objectMessage.serial} already applied on ACK; discarding echo")
-        realtimeObjects.appliedOnAckSerials.remove(objectMessage.serial)
         continue // discard without taking any further action
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (objectMessage.serial != null &&
realtimeObjects.appliedOnAckSerials.contains(objectMessage.serial)) {
Log.d(tag, "RTO9a3: serial ${objectMessage.serial} already applied on ACK; discarding echo")
realtimeObjects.appliedOnAckSerials.remove(objectMessage.serial)
continue // discard without taking any further action
}
if (source == ObjectsOperationSource.CHANNEL &&
objectMessage.serial != null &&
realtimeObjects.appliedOnAckSerials.remove(objectMessage.serial)
) {
Log.d(tag, "RTO9a3: serial ${objectMessage.serial} already applied on ACK; discarding echo")
continue // discard without taking any further action
}
🤖 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/ObjectsManager.kt` around
lines 231 - 236, The ACK-echo dedup block that checks objectMessage.serial
against realtimeObjects.appliedOnAckSerials must only run for channel echoes;
update the conditional to also require the message source be a channel (e.g.
objectMessage.source == MessageSource.CHANNEL or the appropriate enum/constant
used in this codebase) before logging, removing the serial, and continuing, so
LOCAL replays do not clear dedup markers; keep the existing serial check and
remove/continue behavior but nest or extend the if to include the source check
(referencing objectMessage.serial, objectMessage.source, and
realtimeObjects.appliedOnAckSerials).

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