Skip to content

Commit f0b10bd

Browse files
committed
[ECO-5426] Refactored DefaultLiveObjects
1. Added separate classes and impl for DefaultLiveCounter and DefaultMap 2. Added separate class for ObjectPool with ability to handle GC on tombstoned objects
1 parent 2751cfd commit f0b10bd

8 files changed

Lines changed: 488 additions & 97 deletions

File tree

live-objects/src/main/kotlin/io/ably/lib/objects/DefaultLiveObjects.kt

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.ably.lib.objects
22

3+
import io.ably.lib.objects.type.*
4+
import io.ably.lib.objects.type.BaseLiveObject
5+
import io.ably.lib.objects.type.DefaultLiveCounter
36
import io.ably.lib.types.Callback
47
import io.ably.lib.types.ProtocolMessage
58
import io.ably.lib.util.Log
@@ -30,10 +33,12 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
3033
}
3134

3235
private var state = ObjectsState.INITIALIZED
36+
3337
/**
3438
* @spec RTO3 - Objects pool storing all live objects by object ID
3539
*/
36-
private val objectsPool = ConcurrentHashMap<String, LiveObject>()
40+
private val objectsPool = ObjectsPool(adapter)
41+
3742
/**
3843
* @spec RTO5 - Sync objects data pool for collecting sync messages
3944
*/
@@ -239,12 +244,12 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
239244
}
240245

241246
val receivedObjectIds = mutableSetOf<String>()
242-
val existingObjectUpdates = mutableListOf<Pair<LiveObject, Any>>()
247+
val existingObjectUpdates = mutableListOf<Pair<BaseLiveObject, Any>>()
243248

244249
// RTO5c1
245250
for ((objectId, objectState) in syncObjectsDataPool) {
246251
receivedObjectIds.add(objectId)
247-
val existingObject = objectsPool[objectId]
252+
val existingObject = objectsPool.get(objectId)
248253

249254
// RTO5c1a
250255
if (existingObject != null) {
@@ -255,13 +260,12 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
255260
// RTO5c1b
256261
// Create new object
257262
val newObject = createObjectFromState(objectState) // RTO5c1b1
258-
objectsPool[objectId] = newObject
263+
objectsPool.set(objectId, newObject)
259264
}
260265
}
261266

262267
// RTO5c2 - need to remove LiveObject instances from the ObjectsPool for which objectIds were not received during the sync sequence
263-
val objectIdsToRemove = objectsPool.keys.filter { !receivedObjectIds.contains(it) }
264-
objectIdsToRemove.forEach { objectsPool.remove(it) }
268+
objectsPool.deleteExtraObjectIds(receivedObjectIds.toList())
265269

266270
// call subscription callbacks for all updated existing objects
267271
existingObjectUpdates.forEach { (obj, update) ->
@@ -284,9 +288,7 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
284288

285289
val objectOperation: ObjectOperation = objectMessage.operation
286290
// RTO6a - get or create the zero value object in the pool
287-
val obj = objectsPool.getOrPut(objectOperation.objectId) {
288-
createZeroValueObject(objectOperation.objectId)
289-
}
291+
val obj = objectsPool.createZeroValueObjectIfNotExists(objectOperation.objectId)
290292
obj.applyOperation(objectOperation, objectMessage)
291293
}
292294
}
@@ -309,30 +311,16 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
309311
}
310312
}
311313

312-
/**
313-
* Creates a zero-value object.
314-
*
315-
* @spec RTO6 - Creates zero-value objects based on object type
316-
*/
317-
private fun createZeroValueObject(objectId: String): LiveObject {
318-
val objId = ObjectId.fromString(objectId) // RTO6b
319-
val zeroValueObject = when (objId.type) {
320-
ObjectType.Map -> LiveMap.zeroValue(objectId, adapter) // RTO6b2
321-
ObjectType.Counter -> LiveCounter.zeroValue(objectId, adapter) // RTO6b3
322-
}
323-
return zeroValueObject
324-
}
325-
326314
/**
327315
* Creates an object from object state.
328316
*
329-
* @spec RTO5c1b - Creates objects from object state based on type
317+
* @spec RTO5c1b - Creates objects from object state based on type
330318
* TODO - Need to update the implementation
331319
*/
332-
private fun createObjectFromState(objectState: ObjectState): LiveObject {
320+
private fun createObjectFromState(objectState: ObjectState): BaseLiveObject {
333321
return when {
334-
objectState.counter != null -> LiveCounter(objectState.objectId, adapter) // RTO5c1b1a
335-
objectState.map != null -> LiveMap(objectState.objectId, adapter) // RTO5c1b1b
322+
objectState.counter != null -> DefaultLiveCounter(objectState.objectId, objectsPool) // RTO5c1b1a
323+
objectState.map != null -> DefaultLiveMap(objectState.objectId, objectsPool) // RTO5c1b1b
336324
else -> throw serverError("Object state must contain either counter or map data") // RTO5c1b1c
337325
}
338326
}
@@ -375,7 +363,7 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
375363
fun dispose() {
376364
// Dispose of any resources associated with this LiveObjects instance
377365
// For example, close any open connections or clean up references
378-
objectsPool.clear()
366+
objectsPool.dispose()
379367
syncObjectsDataPool.clear()
380368
bufferedObjectOperations.clear()
381369
}

live-objects/src/main/kotlin/io/ably/lib/objects/ObjectMessage.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,7 @@ private fun ObjectValue.size(): Int {
459459
else -> 0 // Spec: OD3f
460460
}
461461
}
462+
463+
internal fun ObjectData?.isInvalid(): Boolean {
464+
return this?.objectId.isNullOrEmpty() && this?.value == null
465+
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package io.ably.lib.objects
2+
3+
import io.ably.lib.objects.type.BaseLiveObject
4+
import io.ably.lib.objects.type.DefaultLiveCounter
5+
import io.ably.lib.objects.type.DefaultLiveMap
6+
import io.ably.lib.util.Log
7+
import kotlinx.coroutines.*
8+
import java.util.concurrent.ConcurrentHashMap
9+
10+
/**
11+
* Constants for ObjectsPool configuration
12+
* Similar to JavaScript DEFAULTS
13+
*/
14+
internal object ObjectsPoolDefaults {
15+
const val GC_INTERVAL_MS = 1000L * 60 * 5 // 5 minutes
16+
/**
17+
* Must be > 2 minutes to ensure we keep tombstones long enough to avoid the possibility of receiving an operation
18+
* with an earlier serial that would not have been applied if the tombstone still existed.
19+
*
20+
* Applies both for map entries tombstones and object tombstones.
21+
*/
22+
const val GC_GRACE_PERIOD_MS = 1000L * 60 * 60 * 24 // 24 hours
23+
}
24+
25+
/**
26+
* Root object ID constant
27+
* Similar to JavaScript ROOT_OBJECT_ID
28+
*/
29+
internal const val ROOT_OBJECT_ID = "root"
30+
31+
/**
32+
* ObjectsPool manages a pool of live objects for a channel.
33+
* Similar to JavaScript ObjectsPool class.
34+
*
35+
* @spec RTO3 - Maintains an objects pool for all live objects on the channel
36+
*/
37+
internal class ObjectsPool(
38+
private val adapter: LiveObjectsAdapter
39+
) {
40+
private val tag = "ObjectsPool"
41+
42+
/**
43+
* @spec RTO3a - Pool storing all live objects by object ID
44+
* Note: This is the same as objectsPool property in DefaultLiveObjects.kt
45+
*/
46+
private val pool = ConcurrentHashMap<String, BaseLiveObject>()
47+
48+
/**
49+
* Coroutine scope for garbage collection
50+
* Similar to JavaScript _gcInterval but using Kotlin coroutines
51+
*/
52+
private val gcScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
53+
54+
/**
55+
* Job for the garbage collection coroutine
56+
*/
57+
private var gcJob: Job? = null
58+
59+
init {
60+
// Initialize pool with root object
61+
createInitialPool()
62+
63+
// Start garbage collection coroutine
64+
startGCJob()
65+
}
66+
67+
/**
68+
* Gets a live object from the pool by object ID.
69+
* Similar to JavaScript get method.
70+
*/
71+
fun get(objectId: String): BaseLiveObject? {
72+
return pool[objectId]
73+
}
74+
75+
/**
76+
* Deletes objects from the pool for which object ids are not found in the provided array of ids.
77+
* Similar to JavaScript deleteExtraObjectIds method.
78+
*/
79+
fun deleteExtraObjectIds(objectIds: List<String>) {
80+
val poolObjectIds = pool.keys.toList()
81+
val extraObjectIds = poolObjectIds.filter { !objectIds.contains(it) }
82+
83+
extraObjectIds.forEach { remove(it) }
84+
}
85+
86+
/**
87+
* Sets a live object in the pool.
88+
* Similar to JavaScript set method.
89+
*/
90+
fun set(objectId: String, liveObject: BaseLiveObject) {
91+
pool[objectId] = liveObject
92+
}
93+
94+
/**
95+
* Removes all objects but root from the pool and clears the data for root.
96+
* Does not create a new root object, so the reference to the root object remains the same.
97+
* Similar to JavaScript resetToInitialPool method.
98+
*/
99+
fun resetToInitialPool(emitUpdateEvents: Boolean) {
100+
// Clear the pool first and keep the root object
101+
val root = pool[ROOT_OBJECT_ID]
102+
if (root != null) {
103+
pool.clear()
104+
pool[ROOT_OBJECT_ID] = root
105+
106+
// Clear the data, this will only clear the root object
107+
clearObjectsData(emitUpdateEvents)
108+
} else {
109+
Log.w(tag, "Root object not found in pool during reset")
110+
}
111+
}
112+
113+
/**
114+
* Clears the data stored for all objects in the pool.
115+
* Similar to JavaScript clearObjectsData method.
116+
*/
117+
fun clearObjectsData(emitUpdateEvents: Boolean) {
118+
for (obj in pool.values) {
119+
val update = obj.clearData()
120+
if (emitUpdateEvents) {
121+
obj.notifyUpdated(update)
122+
}
123+
}
124+
}
125+
126+
/**
127+
* Creates a zero-value object if it doesn't exist in the pool.
128+
* Similar to JavaScript createZeroValueObjectIfNotExists method.
129+
*
130+
* @spec RTO6 - Creates zero-value objects when needed
131+
*/
132+
fun createZeroValueObjectIfNotExists(objectId: String): BaseLiveObject {
133+
val existingObject = get(objectId)
134+
if (existingObject != null) {
135+
return existingObject // RTO6a
136+
}
137+
138+
val parsedObjectId = ObjectId.fromString(objectId) // RTO6b
139+
val zeroValueObject = when (parsedObjectId.type) {
140+
ObjectType.Map -> DefaultLiveMap.zeroValue(objectId, this) // RTO6b2
141+
ObjectType.Counter -> DefaultLiveCounter.zeroValue(objectId, this) // RTO6b3
142+
}
143+
144+
set(objectId, zeroValueObject)
145+
return zeroValueObject
146+
}
147+
148+
/**
149+
* Creates the initial pool with root object.
150+
* Similar to JavaScript _createInitialPool method.
151+
*
152+
* @spec RTO3b - Creates root LiveMap object
153+
*/
154+
private fun createInitialPool() {
155+
val root = DefaultLiveMap.zeroValue(ROOT_OBJECT_ID, this)
156+
pool[ROOT_OBJECT_ID] = root
157+
}
158+
159+
/**
160+
* Garbage collection interval handler.
161+
* Similar to JavaScript _onGCInterval method.
162+
*/
163+
private fun onGCInterval() {
164+
val toDelete = mutableListOf<String>()
165+
166+
for ((objectId, obj) in pool.entries) {
167+
// Tombstoned objects should be removed from the pool if they have been tombstoned for longer than grace period.
168+
// By removing them from the local pool, Objects plugin no longer keeps a reference to those objects, allowing JVM's
169+
// Garbage Collection to eventually free the memory for those objects, provided the user no longer references them either.
170+
if (obj.isTombstoned &&
171+
obj.tombstonedAt != null &&
172+
System.currentTimeMillis() - obj.tombstonedAt!! >= ObjectsPoolDefaults.GC_GRACE_PERIOD_MS) {
173+
toDelete.add(objectId)
174+
continue
175+
}
176+
177+
obj.onGCInterval()
178+
}
179+
180+
toDelete.forEach { pool.remove(it) }
181+
}
182+
183+
/**
184+
* Starts the garbage collection coroutine.
185+
* Similar to JavaScript _gcInterval but using Kotlin coroutines.
186+
*/
187+
private fun startGCJob() {
188+
gcJob = gcScope.launch {
189+
while (isActive) {
190+
try {
191+
onGCInterval()
192+
} catch (e: Exception) {
193+
Log.e(tag, "Error during garbage collection", e)
194+
}
195+
delay(ObjectsPoolDefaults.GC_INTERVAL_MS)
196+
}
197+
}
198+
}
199+
200+
/**
201+
* Disposes of the ObjectsPool, cleaning up resources.
202+
* Should be called when the pool is no longer needed.
203+
*/
204+
fun dispose() {
205+
gcJob?.cancel()
206+
gcScope.cancel()
207+
clear()
208+
}
209+
210+
/**
211+
* Gets all object IDs in the pool.
212+
* Useful for debugging and testing.
213+
*/
214+
fun getObjectIds(): Set<String> = pool.keys.toSet()
215+
216+
/**
217+
* Gets the size of the pool.
218+
* Useful for debugging and testing.
219+
*/
220+
fun size(): Int = pool.size
221+
222+
/**
223+
* Checks if the pool contains an object with the given ID.
224+
*/
225+
fun contains(objectId: String): Boolean = pool.containsKey(objectId)
226+
227+
/**
228+
* Removes an object from the pool.
229+
*/
230+
fun remove(objectId: String): BaseLiveObject? = pool.remove(objectId)
231+
232+
/**
233+
* Clears all objects from the pool.
234+
*/
235+
fun clear() = pool.clear()
236+
}

0 commit comments

Comments
 (0)