feat: Android SDK update for version 23.2.0#121
feat: Android SDK update for version 23.2.0#121ArnabChatterjee20k wants to merge 4 commits intomainfrom
Conversation
Greptile SummaryThis PR bumps the Android SDK to version 23.2.0 and overhauls the Realtime subscription mechanism: subscriptions are now registered via explicit Two bugs in the new queue-based logic need fixing before this is safe to ship:
Confidence Score: 3/5Not 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
|
| 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 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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 inactiveSubscriptions,slot+1=1is →targetSlot=1, writesslotToSubscriptionId[1]="sub_a" - Iteration
"1"→slot=1, is inactiveSubscriptions→targetSlot=1, overwritesslotToSubscriptionId[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.
Made-with: Cursor
Made-with: Cursor
| 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()) | ||
| } |
There was a problem hiding this comment.
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)
This PR contains updates to the Android SDK for version 23.2.0.