Skip to content

Commit ebd5aed

Browse files
committed
[ECO-5426] Added separate managers for handling incoming objectMessages
- Refactored/simplified GC for LiveMapEntries and LiveObjects - Improved BaseLiveObject interface, added comprehensive doc
1 parent 838bf14 commit ebd5aed

9 files changed

Lines changed: 345 additions & 335 deletions

File tree

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

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

3-
import io.ably.lib.objects.type.*
43
import io.ably.lib.objects.type.BaseLiveObject
5-
import io.ably.lib.objects.type.DefaultLiveCounter
4+
import io.ably.lib.objects.type.livecounter.DefaultLiveCounter
5+
import io.ably.lib.objects.type.livemap.DefaultLiveMap
66
import io.ably.lib.types.Callback
77
import io.ably.lib.types.ProtocolMessage
88
import io.ably.lib.util.Log
@@ -246,7 +246,7 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
246246
// RTO5c1a
247247
if (existingObject != null) {
248248
// Update existing object
249-
val update = existingObject.overrideWithObjectState(objectState) // RTO5c1a1
249+
val update = existingObject.applyObjectState(objectState) // RTO5c1a1
250250
existingObjectUpdates.add(Pair(existingObject, update))
251251
} else { // RTO5c1b
252252
// RTO5c1b1 - Create new object and add it to the pool
@@ -323,7 +323,7 @@ internal class DefaultLiveObjects(private val channelName: String, private val a
323323
objectState.map != null -> DefaultLiveMap.zeroValue(objectState.objectId, adapter, objectsPool) // RTO5c1b1b
324324
else -> throw clientError("Object state must contain either counter or map data") // RTO5c1b1c
325325
}.apply {
326-
overrideWithObjectState(objectState)
326+
applyObjectState(objectState)
327327
}
328328
}
329329

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

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

33
import io.ably.lib.objects.type.BaseLiveObject
4-
import io.ably.lib.objects.type.DefaultLiveCounter
5-
import io.ably.lib.objects.type.DefaultLiveMap
4+
import io.ably.lib.objects.type.livecounter.DefaultLiveCounter
5+
import io.ably.lib.objects.type.livemap.DefaultLiveMap
66
import io.ably.lib.util.Log
77
import kotlinx.coroutines.*
88
import java.util.concurrent.ConcurrentHashMap
@@ -71,9 +71,7 @@ internal class ObjectsPool(
7171
* Deletes objects from the pool for which object ids are not found in the provided array of ids.
7272
*/
7373
fun deleteExtraObjectIds(objectIds: MutableSet<String>) {
74-
pool.keys.toList()
75-
.filter { it !in objectIds }
76-
.forEach { remove(it) }
74+
pool.entries.removeIf { (key, _) -> key !in objectIds }
7775
}
7876

7977
/**
@@ -104,7 +102,7 @@ internal class ObjectsPool(
104102
/**
105103
* Clears the data stored for all objects in the pool.
106104
*/
107-
fun clearObjectsData(emitUpdateEvents: Boolean) {
105+
private fun clearObjectsData(emitUpdateEvents: Boolean) {
108106
for (obj in pool.values) {
109107
val update = obj.clearData()
110108
if (emitUpdateEvents) {
@@ -118,7 +116,7 @@ internal class ObjectsPool(
118116
*
119117
* @spec RTO6 - Creates zero-value objects when needed
120118
*/
121-
fun createZeroValueObjectIfNotExists(objectId: String): BaseLiveObject {
119+
internal fun createZeroValueObjectIfNotExists(objectId: String): BaseLiveObject {
122120
val existingObject = get(objectId)
123121
if (existingObject != null) {
124122
return existingObject // RTO6a
@@ -148,23 +146,13 @@ internal class ObjectsPool(
148146
* Garbage collection interval handler.
149147
*/
150148
private fun onGCInterval() {
151-
val toDelete = mutableListOf<String>()
152-
153-
for ((objectId, obj) in pool.entries) {
154-
// Tombstoned objects should be removed from the pool if they have been tombstoned for longer than grace period.
155-
// By removing them from the local pool, Objects plugin no longer keeps a reference to those objects, allowing JVM's
156-
// Garbage Collection to eventually free the memory for those objects, provided the user no longer references them either.
157-
if (obj.isTombstoned &&
158-
obj.tombstonedAt != null &&
159-
System.currentTimeMillis() - obj.tombstonedAt!! >= ObjectsPoolDefaults.GC_GRACE_PERIOD_MS) {
160-
toDelete.add(objectId)
161-
continue
149+
pool.entries.removeIf { (_, obj) ->
150+
if (obj.isEligibleForGc()) { true } // Remove from pool
151+
else {
152+
obj.onGCInterval()
153+
false // Keep in pool
162154
}
163-
164-
obj.onGCInterval()
165155
}
166-
167-
toDelete.forEach { pool.remove(it) }
168156
}
169157

170158
/**
Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,32 @@
11
package io.ably.lib.objects.type
22

3-
import io.ably.lib.objects.LiveObjectsAdapter
3+
import io.ably.lib.objects.*
44
import io.ably.lib.objects.ObjectMessage
55
import io.ably.lib.objects.ObjectOperation
66
import io.ably.lib.objects.ObjectState
7-
import io.ably.lib.objects.ObjectsPool
7+
import io.ably.lib.objects.ObjectsPoolDefaults
88
import io.ably.lib.objects.objectError
99
import io.ably.lib.util.Log
1010

1111
/**
1212
* Base implementation of LiveObject interface.
1313
* Provides common functionality for all live objects.
1414
*
15-
* @spec RTLO1/RTLO2 - Base class for LiveMap/LiveCounter objects
15+
* @spec RTLO1/RTLO2 - Base class for LiveMap/LiveCounter object
1616
*/
1717
internal abstract class BaseLiveObject(
18-
protected val objectId: String, // // RTLO3a
18+
internal val objectId: String, // // RTLO3a
1919
protected val adapter: LiveObjectsAdapter
2020
) {
2121

2222
protected open val tag = "BaseLiveObject"
23-
internal var isTombstoned = false
24-
internal var tombstonedAt: Long? = null
2523

26-
protected val siteTimeserials = mutableMapOf<String, String>() // RTLO3b
24+
internal val siteTimeserials = mutableMapOf<String, String>() // RTLO3b
2725

28-
/**
29-
* @spec RTLO3 - Flag to track if create operation has been merged for LiveMap/LiveCounter
30-
*/
31-
protected var createOperationIsMerged = false // RTLO3c
26+
internal var createOperationIsMerged = false // RTLO3c
27+
28+
internal var isTombstoned = false
29+
private var tombstonedAt: Long? = null
3230

3331
fun notifyUpdated(update: Any) {
3432
// TODO: Implement event emission for updates
@@ -40,7 +38,7 @@ internal abstract class BaseLiveObject(
4038
*
4139
* @spec RTLO4a - Serial comparison logic for LiveMap/LiveCounter operations
4240
*/
43-
protected fun canApplyOperation(siteCode: String?, serial: String?): Boolean {
41+
internal fun canApplyOperation(siteCode: String?, serial: String?): Boolean {
4442
if (serial.isNullOrEmpty()) {
4543
throw objectError("Invalid serial: $serial") // RTLO4a3
4644
}
@@ -51,41 +49,31 @@ internal abstract class BaseLiveObject(
5149
return existingSiteSerial == null || serial > existingSiteSerial // RTLO4a5, RTLO4a6
5250
}
5351

54-
/**
55-
* Updates the time serial for a given site code.
56-
*/
57-
protected fun updateTimeSerial(opSiteCode: String, opSerial: String) {
58-
siteTimeserials[opSiteCode] = opSerial
59-
}
60-
61-
/**
62-
* Applies object delete operation.
63-
*
64-
* @spec RTLM10/RTLC10 - Object deletion for LiveMap/LiveCounter
65-
*/
66-
protected fun applyObjectDelete(): Any {
67-
return tombstone()
68-
}
69-
7052
/**
7153
* Marks the object as tombstoned.
72-
*
73-
* @spec RTLM11/RTLC11 - Tombstone functionality for LiveMap/LiveCounter
7454
*/
75-
protected fun tombstone(): Any {
55+
internal fun tombstone(): Any {
7656
isTombstoned = true
7757
tombstonedAt = System.currentTimeMillis()
7858
val update = clearData()
7959
// TODO: Emit lifecycle events
8060
return update
8161
}
8262

63+
/**
64+
* Checks if the object is eligible for garbage collection.
65+
*/
66+
internal fun isEligibleForGc(): Boolean {
67+
val currentTime = System.currentTimeMillis()
68+
return isTombstoned && tombstonedAt?.let { currentTime - it >= ObjectsPoolDefaults.GC_GRACE_PERIOD_MS } == true
69+
}
70+
8371
/**
8472
* This is invoked by ObjectMessage having updated data with parent `ProtocolMessageAction` as `object_sync`
8573
* @return an update describing the changes
8674
* @spec RTLM6/RTLC6 - Overrides ObjectMessage with object data state from sync to LiveMap/LiveCounter
8775
*/
88-
abstract fun overrideWithObjectState(objectState: ObjectState): Map<String, Any>
76+
abstract fun applyObjectState(objectState: ObjectState): Map<String, Any>
8977

9078
/**
9179
* This is invoked by ObjectMessage having updated data with parent `ProtocolMessageAction` as `object`
@@ -96,11 +84,28 @@ internal abstract class BaseLiveObject(
9684

9785
/**
9886
* Clears the object's data and returns an update describing the changes.
87+
* This is called during tombstoning and explicit clear operations.
88+
*
89+
* This method:
90+
* 1. Calculates a diff between the current state and an empty state
91+
* 2. Clears all entries from the underlying data structure
92+
* 3. Returns a map containing metadata about what was cleared
93+
*
94+
* The returned map is used to notifying other components about what entries were removed.
95+
*
96+
* @return A map representing the diff of changes made
9997
*/
10098
abstract fun clearData(): Map<String, Any>
10199

102100
/**
103-
* Called during garbage collection intervals.
101+
* Called during garbage collection intervals to clean up expired entries.
102+
*
103+
* This method should identify and remove entries that:
104+
* - Have been marked as tombstoned
105+
* - Have a tombstone timestamp older than the configured grace period
106+
*
107+
* Implementations typically use single-pass removal techniques to
108+
* efficiently clean up expired data without creating temporary collections.
104109
*/
105110
abstract fun onGCInterval()
106111
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package io.ably.lib.objects.type.livecounter
2+
3+
import io.ably.lib.objects.*
4+
import io.ably.lib.objects.ObjectMessage
5+
import io.ably.lib.objects.ObjectOperation
6+
import io.ably.lib.objects.ObjectState
7+
import io.ably.lib.objects.type.BaseLiveObject
8+
import io.ably.lib.types.Callback
9+
10+
/**
11+
* Implementation of LiveObject for LiveCounter.
12+
*
13+
* @spec RTLC1/RTLC2 - LiveCounter implementation extends LiveObject
14+
*/
15+
internal class DefaultLiveCounter(
16+
objectId: String,
17+
adapter: LiveObjectsAdapter,
18+
) : LiveCounter, BaseLiveObject(objectId, adapter) {
19+
20+
override val tag = "LiveCounter"
21+
22+
/**
23+
* Counter data value
24+
*/
25+
internal var data: Long = 0 // RTLC3
26+
27+
/**
28+
* liveCounterManager instance for managing LiveMap operations
29+
*/
30+
private val liveCounterManager = LiveCounterManager(this)
31+
32+
override fun increment() {
33+
TODO("Not yet implemented")
34+
}
35+
36+
override fun incrementAsync(callback: Callback<Void>) {
37+
TODO("Not yet implemented")
38+
}
39+
40+
override fun decrement() {
41+
TODO("Not yet implemented")
42+
}
43+
44+
override fun decrementAsync(callback: Callback<Void>) {
45+
TODO("Not yet implemented")
46+
}
47+
48+
/**
49+
* @spec RTLC5 - Returns the current counter value
50+
*/
51+
override fun value(): Long {
52+
// RTLC5a, RTLC5b - Configuration validation would be done here
53+
return data // RTLC5c
54+
}
55+
56+
override fun applyObjectState(objectState: ObjectState): Map<String, Long> {
57+
return liveCounterManager.applyObjectState(objectState)
58+
}
59+
60+
override fun applyOperation(operation: ObjectOperation, message: ObjectMessage) {
61+
liveCounterManager.applyOperation(operation, message)
62+
}
63+
64+
override fun clearData(): Map<String, Long> {
65+
return mapOf("amount" to data).apply { data = 0 }
66+
}
67+
68+
override fun onGCInterval() {
69+
// Nothing to GC for a counter object
70+
return
71+
}
72+
73+
companion object {
74+
/**
75+
* Creates a zero-value counter object.
76+
* @spec RTLC4 - Returns LiveCounter with 0 value
77+
*/
78+
internal fun zeroValue(objectId: String, adapter: LiveObjectsAdapter): DefaultLiveCounter {
79+
return DefaultLiveCounter(objectId, adapter)
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)