Skip to content

Commit b8bf66d

Browse files
committed
[ECO-5380] Updated LiveObjectsAdaopter interface
1. Added and implemented method to calculate maxMessageSizeLimit 2. Added extension method ensureMessageSizeWithinLimit to validate object msg sizes 3. Added unit test to validate objectMessageSizeWithinLimit
1 parent 8cacb6c commit b8bf66d

10 files changed

Lines changed: 85 additions & 36 deletions

File tree

lib/src/main/java/io/ably/lib/objects/Adapter.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ public void send(@NotNull ProtocolMessage msg, @NotNull CompletionListener liste
2929
// Always queue LiveObjects messages to ensure reliable state synchronization and proper acknowledgment
3030
ably.connection.connectionManager.send(msg, true, listener);
3131
}
32+
33+
@Override
34+
public long maxMessageSizeLimit() {
35+
return ably.connection.connectionManager.maxMessageSize;
36+
}
3237
}

lib/src/main/java/io/ably/lib/objects/LiveObjectsAdapter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,13 @@ public interface LiveObjectsAdapter {
2323
* @param channelSerial the serial to set for the channel
2424
*/
2525
void setChannelSerial(@NotNull String channelName, @NotNull String channelSerial);
26+
27+
/**
28+
* Retrieves the maximum message size allowed for the messages.
29+
* This method returns the maximum size in bytes that a message can have.
30+
*
31+
* @return the maximum message size limit in bytes.
32+
*/
33+
long maxMessageSizeLimit();
2634
}
2735

lib/src/main/java/io/ably/lib/transport/ConnectionManager.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,7 @@ private synchronized void onConnected(ProtocolMessage message) {
12861286
connection.key = connectionDetails.connectionKey; //RTN16d
12871287
maxIdleInterval = connectionDetails.maxIdleInterval;
12881288
connectionStateTtl = connectionDetails.connectionStateTtl;
1289+
maxMessageSize = connectionDetails.maxMessageSize;
12891290

12901291
/* set the clientId resolved from token, if any */
12911292
String clientId = connectionDetails.clientId;
@@ -1981,6 +1982,7 @@ private boolean isFatalError(ErrorInfo err) {
19811982
private long lastActivity;
19821983
private CMConnectivityListener connectivityListener;
19831984
private long connectionStateTtl = Defaults.connectionStateTtl;
1985+
public long maxMessageSize = Defaults.maxMessageSize;
19841986
long maxIdleInterval = Defaults.maxIdleInterval;
19851987
private int disconnectedRetryAttempt = 0;
19861988

lib/src/main/java/io/ably/lib/transport/Defaults.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public class Defaults {
5252
public static long fallbackRetryTimeout = 10*60*1000L;
5353
/* CD2h (but no default in the spec) */
5454
public static long maxIdleInterval = 20000L;
55+
// 64kB, as per CD2c
56+
public static long maxMessageSize = 65536L;
5557
/* DF1a */
5658
public static long connectionStateTtl = 120000L;
5759

lib/src/main/java/io/ably/lib/types/Message.java

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -519,29 +519,6 @@ protected void read(final JsonObject map) throws MessageDecodeException {
519519
}
520520
}
521521

522-
/**
523-
* Calculates the size of the message.
524-
* Spec: TM6
525-
*/
526-
protected int size() {
527-
// Spec: TM6a - Sum of sizes of name, data, clientId, and extras
528-
int nameSize = name != null ? name.length() : 0; // Spec: TM6a
529-
int dataSize = 0;
530-
531-
if (data != null) {
532-
if (data instanceof byte[]) {
533-
dataSize = ((byte[]) data).length; // Spec: TM6c
534-
} else {
535-
dataSize = Serialisation.gson.toJson(data).length(); // Spec: TM6b
536-
}
537-
}
538-
539-
int clientIdSize = clientId != null ? clientId.length() : 0; // Spec: TM6f
540-
int extrasSize = extras != null ? Serialisation.gson.toJson(extras).length() : 0; // Spec: TM6d
541-
542-
return nameSize + dataSize + clientIdSize + extrasSize;
543-
}
544-
545522
public static class Serializer implements JsonSerializer<Message>, JsonDeserializer<Message> {
546523
@Override
547524
public JsonElement serialize(Message message, Type typeOfMessage, JsonSerializationContext ctx) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.ably.lib.objects
33
internal enum class ErrorCode(public val code: Int) {
44
BadRequest(40_000),
55
InternalError(50_000),
6+
MaxMessageSizeExceeded(40_009),
67
}
78

89
internal enum class HttpStatusCode(public val code: Int) {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ internal suspend fun LiveObjectsAdapter.sendAsync(message: ProtocolMessage) = su
2323
}
2424
}
2525

26+
internal fun LiveObjectsAdapter.ensureMessageSizeWithinLimit(objectMessages: Array<ObjectMessage>) {
27+
val maximumAllowedSize = maxMessageSizeLimit()
28+
val objectsTotalMessageSize = objectMessages.sumOf { it.size() }
29+
if (objectsTotalMessageSize > maximumAllowedSize) {
30+
throw ablyException("ObjectMessage size $objectsTotalMessageSize exceeds maximum allowed size of $maximumAllowedSize bytes",
31+
ErrorCode.MaxMessageSizeExceeded)
32+
}
33+
}
34+
2635
internal enum class ProtocolMessageFormat(private val value: String) {
2736
Msgpack("msgpack"),
2837
Json("json");

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ internal data class ObjectMessage(
339339
* Calculates the size of an ObjectMessage in bytes.
340340
* Spec: OM3
341341
*/
342-
internal fun ObjectMessage.size(): Int {
342+
internal fun ObjectMessage.size(): Long {
343343
val clientIdSize = clientId?.length ?: 0 // Spec: OM3f
344344
val operationSize = operation?.size() ?: 0 // Spec: OM3b, OOP4
345345
val objectStateSize = objectState?.size() ?: 0 // Spec: OM3c, OST3
@@ -352,7 +352,7 @@ internal fun ObjectMessage.size(): Int {
352352
* Calculates the size of an ObjectOperation in bytes.
353353
* Spec: OOP4
354354
*/
355-
private fun ObjectOperation.size(): Int {
355+
private fun ObjectOperation.size(): Long {
356356
val mapOpSize = mapOp?.size() ?: 0 // Spec: OOP4b, OMO3
357357
val counterOpSize = counterOp?.size() ?: 0 // Spec: OOP4c, OCO3
358358
val mapSize = map?.size() ?: 0 // Spec: OOP4d, OMP4
@@ -365,7 +365,7 @@ private fun ObjectOperation.size(): Int {
365365
* Calculates the size of an ObjectState in bytes.
366366
* Spec: OST3
367367
*/
368-
private fun ObjectState.size(): Int {
368+
private fun ObjectState.size(): Long {
369369
val mapSize = map?.size() ?: 0 // Spec: OST3b, OMP4
370370
val counterSize = counter?.size() ?: 0 // Spec: OST3c, OCN3
371371
val createOpSize = createOp?.size() ?: 0 // Spec: OST3d, OOP4
@@ -374,10 +374,10 @@ private fun ObjectState.size(): Int {
374374
}
375375

376376
/**
377-
* Calculates the size of an ObjectMap in bytes.
377+
* Calculates the size of an ObjectMapOp in bytes.
378378
* Spec: OMO3
379379
*/
380-
private fun ObjectMapOp.size(): Int {
380+
private fun ObjectMapOp.size(): Long {
381381
val keySize = key.length // Spec: OMO3d - Size of the key
382382
val dataSize = data?.size() ?: 0 // Spec: OMO3b - Size of the data, calculated per "OD3"
383383
return keySize + dataSize
@@ -387,7 +387,7 @@ private fun ObjectMapOp.size(): Int {
387387
* Calculates the size of a CounterOp in bytes.
388388
* Spec: OCO3
389389
*/
390-
private fun ObjectCounterOp.size(): Int {
390+
private fun ObjectCounterOp.size(): Long {
391391
// Size is 8 if amount is a number, 0 if amount is null or omitted
392392
return if (amount != null) 8 else 0 // Spec: OCO3a, OCO3b
393393
}
@@ -396,7 +396,7 @@ private fun ObjectCounterOp.size(): Int {
396396
* Calculates the size of an ObjectMap in bytes.
397397
* Spec: OMP4
398398
*/
399-
private fun ObjectMap.size(): Int {
399+
private fun ObjectMap.size(): Long {
400400
// Calculate the size of all map entries in the map property
401401
val entriesSize = entries?.entries?.sumOf {
402402
it.key.length + it.value.size() // // Spec: OMP4a1, OMP4a2
@@ -409,7 +409,7 @@ private fun ObjectMap.size(): Int {
409409
* Calculates the size of an ObjectCounter in bytes.
410410
* Spec: OCN3
411411
*/
412-
private fun ObjectCounter.size(): Int {
412+
private fun ObjectCounter.size(): Long {
413413
// Size is 8 if count is a number, 0 if count is null or omitted
414414
return if (count != null) 8 else 0
415415
}
@@ -418,7 +418,7 @@ private fun ObjectCounter.size(): Int {
418418
* Calculates the size of a MapEntry in bytes.
419419
* Spec: OME3
420420
*/
421-
private fun ObjectMapEntry.size(): Int {
421+
private fun ObjectMapEntry.size(): Long {
422422
// The size is equal to the size of the data property, calculated per "OD3"
423423
return data?.size() ?: 0
424424
}
@@ -427,20 +427,20 @@ private fun ObjectMapEntry.size(): Int {
427427
* Calculates the size of an ObjectData in bytes.
428428
* Spec: OD3
429429
*/
430-
private fun ObjectData.size(): Int {
430+
private fun ObjectData.size(): Long {
431431
return value?.size() ?: 0 // Spec: OD3f
432432
}
433433

434434
/**
435435
* Calculates the size of an ObjectValue in bytes.
436436
* Spec: OD3*
437437
*/
438-
private fun ObjectValue.size(): Int {
438+
private fun ObjectValue.size(): Long {
439439
return when (value) {
440440
is Boolean -> 1 // Spec: OD3b
441-
is Binary -> value.size() // Spec: OD3c
441+
is Binary -> value.size().toLong() // Spec: OD3c
442442
is Number -> 8 // Spec: OD3d
443-
is String -> value.length // Spec: OD3e
443+
is String -> value.byteSize.toLong() // Spec: OD3e
444444
else -> 0 // Spec: OD3f
445445
}
446446
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ private fun createAblyException(
3333
internal fun clientError(errorMessage: String) = ablyException(errorMessage, ErrorCode.BadRequest, HttpStatusCode.BadRequest)
3434

3535
internal fun serverError(errorMessage: String) = ablyException(errorMessage, ErrorCode.InternalError, HttpStatusCode.InternalServerError)
36+
37+
/**
38+
* Calculates the byte size of a string.
39+
* For non-ASCII, the byte size can be 2–4x the character count. For ASCII, there is no difference.
40+
*/
41+
internal val String.byteSize: Int
42+
get() = this.toByteArray(Charsets.UTF_8).size

live-objects/src/test/kotlin/io/ably/lib/objects/unit/LiveObjectTest.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
package io.ably.lib.objects.unit
22

3+
import io.ably.lib.objects.*
4+
import io.ably.lib.objects.ObjectMessage
5+
import io.ably.lib.objects.size
6+
import io.ably.lib.types.AblyException
7+
import io.mockk.every
8+
import io.mockk.mockk
39
import kotlinx.coroutines.test.runTest
410
import org.junit.Test
11+
import kotlin.test.assertEquals
12+
import kotlin.test.assertFailsWith
513
import kotlin.test.assertNotNull
614

715
class LiveObjectTest {
@@ -11,4 +19,34 @@ class LiveObjectTest {
1119
val objects = channel.objects
1220
assertNotNull(objects)
1321
}
22+
23+
@Test
24+
fun testObjectMessageSizeWithinLimit() = runTest {
25+
val mockAdapter = mockk<LiveObjectsAdapter>()
26+
every { mockAdapter.maxMessageSizeLimit() } returns 65536L // 64 kb
27+
assertEquals(65536L, mockAdapter.maxMessageSizeLimit())
28+
29+
// Create ObjectMessage with dummy data that results in size 60 kb which is within the limit
30+
val objectMessage = ObjectMessage(
31+
clientId = CharArray(60 * 1024) { ('a'..'z').random() }.concatToString()
32+
)
33+
assertEquals(60 * 1024L, objectMessage.size()) // 60 kb (61,440 characters)
34+
// Doesn't throw exception, so test doesn't fail
35+
mockAdapter.ensureMessageSizeWithinLimit(arrayOf(objectMessage))
36+
37+
// Create ObjectMessage with dummy data that results in size 5kb
38+
val objectMessage2 = ObjectMessage(
39+
clientId = CharArray(5 * 1024) { ('a'..'z').random() }.concatToString()
40+
)
41+
assertEquals(5 * 1024L, objectMessage2.size()) // 5 kb
42+
43+
val exception = assertFailsWith<AblyException> {
44+
// both messages together with size of 65kb exceed the limit
45+
mockAdapter.ensureMessageSizeWithinLimit(arrayOf(objectMessage, objectMessage2))
46+
}
47+
// Assert on error code and message
48+
assertEquals(40009, exception.errorInfo.code)
49+
val expectedMessage = "ObjectMessage size 66560 exceeds maximum allowed size of 65536 bytes"
50+
assertEquals(expectedMessage, exception.errorInfo.message)
51+
}
1452
}

0 commit comments

Comments
 (0)