Skip to content

feat: Android SDK update for version 23.2.0#121

Open
ArnabChatterjee20k wants to merge 4 commits intomainfrom
dev
Open

feat: Android SDK update for version 23.2.0#121
ArnabChatterjee20k wants to merge 4 commits intomainfrom
dev

Conversation

@ArnabChatterjee20k
Copy link
Copy Markdown
Member

@ArnabChatterjee20k ArnabChatterjee20k commented Apr 20, 2026

This PR contains updates to the Android SDK for version 23.2.0.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 20, 2026

Greptile Summary

This PR bumps the Android SDK to version 23.2.0 and overhauls the Realtime subscription mechanism: subscriptions are now registered via explicit subscribe messages with a FIFO pendingSubscribeSlotsQueue to correlate server responses back to local slot indices, replacing the fragile slot+1 heuristic from the previous iteration.

Two bugs in the new queue-based logic need fixing before this is safe to ship:

  • pendingSubscribeSlotsQueue is never cleared when the socket closes/reconnects, so stale entries left over from the previous connection misalign responses to slot snapshots, silently dropping events.
  • sendSubscribeMessage has two separate synchronized blocks; a concurrent call path from handleResponseConnected can enqueue slots and send the WebSocket frame out of order, corrupting subscription ID mappings.

Confidence Score: 3/5

Not safe to merge — two P1 bugs in Realtime.kt can cause silent event-dispatch failures on reconnect and under concurrency.

Both P1 findings are on the critical Realtime path: the missing queue clear on reconnect is a deterministic bug (every reconnect after an in-flight subscribe will misalign IDs), and the TOCTOU race is reproducible under normal concurrent subscribe/reconnect patterns. Either can result in events being silently dropped for one or more subscriptions.

library/src/main/java/io/appwrite/services/Realtime.kt — specifically closeSocket() and sendSubscribeMessage()

Important Files Changed

Filename Overview
library/src/main/java/io/appwrite/services/Realtime.kt Major rework of subscription ID tracking: adds explicit subscribe messages, a FIFO queue for pending slot snapshots, and a new handleResponseAction handler. Two bugs: pendingSubscribeSlotsQueue is never cleared on reconnect (stale entries misalign future responses), and sendSubscribeMessage has a TOCTOU gap between its two synchronized blocks that can invert queue/send order under concurrency.
library/src/main/java/io/appwrite/Client.kt SDK version bumped from 23.1.0 to 23.2.0; x-appwrite-response-format header unchanged at 1.9.1.
README.md Dependency version updated to 23.2.0; compatibility note changed to vague "latest" server version (already flagged in prior thread).
CHANGELOG.md New 23.2.0 entry added documenting Realtime subscribe message changes and reconnect fix.
library/src/main/java/io/appwrite/services/Account.kt Trailing newline added at end of file — no logic changes.
library/src/main/java/io/appwrite/services/Databases.kt Trailing newline added — no logic changes.
library/src/main/java/io/appwrite/services/TablesDB.kt Trailing newline added — no logic changes.

Comments Outside Diff (1)

  1. library/src/main/java/io/appwrite/services/Realtime.kt, line 102-105 (link)

    P1 pendingSubscribeSlotsQueue never cleared on reconnect

    closeSocket() (and createSocket()) never drains pendingSubscribeSlotsQueue. If any in-flight subscribe message was queued before the socket dropped, its entry survives into the new connection. On reconnect sendSubscribeMessage() appends fresh slot snapshots, so the queue head is now a stale snapshot from the previous session. The first handleResponseAction response pops that stale entry and maps the new server-issued subscription IDs to the wrong slots — some slots silently receive no ID while others get a mismatched one, causing events for those channels to never be dispatched.

    Fix: drain the queue when the socket closes, e.g.:

    private fun closeSocket() {
        stopHeartbeat()
        synchronized(subscriptionLock) {
            pendingSubscribeSlotsQueue.clear()
        }
        socket?.close(RealtimeCode.POLICY_VIOLATION.value, null)
    }

Reviews (3): Last reviewed commit: "chore: restore .github metadata" | Re-trigger Greptile

Comment thread library/src/main/java/io/appwrite/Client.kt Outdated
Comment thread README.md Outdated
@ArnabChatterjee20k ArnabChatterjee20k changed the title feat: Android SDK update for version 23.1.0 feat: Android SDK update for version 23.2.0 Apr 20, 2026
Comment on lines +300 to 311
val slot = (slotStr as? String)?.toIntOrNull()
if (slot != null && subscriptionId is String) {
val targetSlot = when {
activeSubscriptions.containsKey(slot) -> slot
activeSubscriptions.containsKey(slot + 1) -> slot + 1
else -> slot
}
slotToSubscriptionId[targetSlot] = subscriptionId
subscriptionIdToSlot[subscriptionId] = targetSlot
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 slot+1 heuristic causes subscription ID collision

When the server sends zero-based slot indices (e.g. "0" and "1") and the client has one-based slots (e.g. 1 and 2, since subscriptionsCounter.incrementAndGet() starts at 1), both iterations end up mapping to client slot 1.

Concrete failure trace with two active subscriptions (slots 1 and 2) and server response {"0": "sub_a", "1": "sub_b"}:

  • Iteration "0"slot=0, not in activeSubscriptions, slot+1=1 istargetSlot=1, writes slotToSubscriptionId[1]="sub_a"
  • Iteration "1"slot=1, is in activeSubscriptionstargetSlot=1, overwrites slotToSubscriptionId[1]="sub_b", leaving slot 2 with no subscription ID and "sub_a" silently dropped

Events for slot 2 will never dispatch; one subscription goes dark without any error.

Comment on lines +107 to +137
private fun sendSubscribeMessage() {
val ws = socket ?: return
val rows = mutableListOf<Map<String, Any>>()
val slotsForMessage = mutableListOf<Int>()

synchronized(subscriptionLock) {
activeSubscriptions.forEach { (slot, subscription) ->
val queries = subscription.queries
val row = mutableMapOf<String, Any>(
"channels" to subscription.channels.toList(),
"queries" to queries.toList()
)
val knownSubscriptionId = slotToSubscriptionId[slot]
if (!knownSubscriptionId.isNullOrEmpty()) {
row["subscriptionId"] = knownSubscriptionId
}
rows.add(row)
slotsForMessage.add(slot)
}
}

if (rows.isEmpty()) {
return
}

synchronized(subscriptionLock) {
pendingSubscribeSlotsQueue.addLast(slotsForMessage.toList())
}

ws.send(mapOf("type" to "subscribe", "data" to rows).toJson())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 TOCTOU gap between queue-add and WebSocket send

sendSubscribeMessage uses two separate synchronized(subscriptionLock) blocks: the first captures the slot snapshot; the second appends it to pendingSubscribeSlotsQueue. ws.send(...) is then called outside both locks. When sendSubscribeMessage is invoked from handleResponseConnected (no outer lock held), a concurrent subscribe() call — which holds the outer lock throughout its own reentrant sendSubscribeMessage — can interleave such that the queue order and the network send order diverge, causing handleResponseAction to match server responses to the wrong slot snapshots and corrupt subscription ID mappings.

The safest fix is to merge both synchronized blocks and compute the JSON inside the lock:

val message: String
synchronized(subscriptionLock) {
    // ... build rows / slotsForMessage ...
    if (rows.isEmpty()) return
    pendingSubscribeSlotsQueue.addLast(slotsForMessage.toList())
    message = mapOf("type" to "subscribe", "data" to rows).toJson()
}
ws.send(message)

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.

1 participant