Skip to content

Commit 80a655e

Browse files
authored
Merge pull request #1097 from ably/liveobjects/message-size
[ECO-5380] Liveobjects: Message size
2 parents 64a4d84 + b04c4cf commit 80a655e

File tree

11 files changed

+388
-24
lines changed

11 files changed

+388
-24
lines changed

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 int 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+
int 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 int 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 int maxMessageSize = 65536;
5557
/* DF1a */
5658
public static long connectionStateTtl = 120000L;
5759

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public class ConnectionDetails {
4242
* <p>
4343
* Spec: CD2c
4444
*/
45-
public Long maxMessageSize;
45+
public int maxMessageSize;
4646
/**
4747
* The maximum allowable number of requests per second from a client or Ably.
4848
* In the case of a realtime connection, this restriction applies to the number of messages sent,
@@ -97,7 +97,7 @@ ConnectionDetails readMsgpack(MessageUnpacker unpacker) throws IOException {
9797
serverId = unpacker.unpackString();
9898
break;
9999
case "maxMessageSize":
100-
maxMessageSize = unpacker.unpackLong();
100+
maxMessageSize = unpacker.unpackInt();
101101
break;
102102
case "maxInboundRate":
103103
maxInboundRate = unpacker.unpackLong();

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: 13 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");
@@ -41,3 +50,7 @@ internal class Binary(val data: ByteArray?) {
4150
return data?.contentHashCode() ?: 0
4251
}
4352
}
53+
54+
internal fun Binary.size(): Int {
55+
return data?.size ?: 0
56+
}

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

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

3+
import com.google.gson.JsonArray
4+
import com.google.gson.JsonObject
5+
36
/**
47
* An enum class representing the different actions that can be performed on an object.
58
* Spec: OOP2
@@ -15,7 +18,7 @@ internal enum class ObjectOperationAction(val code: Int) {
1518

1619
/**
1720
* An enum class representing the conflict-resolution semantics used by a Map object.
18-
* Spec: MAP2
21+
* Spec: OMP2
1922
*/
2023
internal enum class MapSemantics(val code: Int) {
2124
LWW(0);
@@ -42,91 +45,117 @@ internal data class ObjectData(
4245
* String, number, boolean or binary - a concrete value of the object
4346
* Spec: OD2c
4447
*/
45-
val value: Any? = null,
48+
val value: ObjectValue? = null,
4649
)
4750

51+
/**
52+
* Represents a value that can be a String, Number, Boolean, Binary, JsonObject or JsonArray.
53+
* Performs a type check on initialization.
54+
* Spec: OD2c
55+
*/
56+
internal data class ObjectValue(
57+
/**
58+
* The concrete value of the object. Can be a String, Number, Boolean, Binary, JsonObject or JsonArray.
59+
* Spec: OD2c
60+
*/
61+
val value: Any,
62+
) {
63+
init {
64+
require(
65+
value is String ||
66+
value is Number ||
67+
value is Boolean ||
68+
value is Binary ||
69+
value is JsonObject ||
70+
value is JsonArray
71+
) {
72+
"value must be String, Number, Boolean, Binary, JsonObject or JsonArray"
73+
}
74+
}
75+
}
76+
4877
/**
4978
* A MapOp describes an operation to be applied to a Map object.
50-
* Spec: MOP1
79+
* Spec: OMO1
5180
*/
52-
internal data class MapOp(
81+
internal data class ObjectMapOp(
5382
/**
5483
* The key of the map entry to which the operation should be applied.
55-
* Spec: MOP2a
84+
* Spec: OMO2a
5685
*/
5786
val key: String,
5887

5988
/**
6089
* The data that the map entry should contain if the operation is a MAP_SET operation.
61-
* Spec: MOP2b
90+
* Spec: OMO2b
6291
*/
6392
val data: ObjectData? = null
6493
)
6594

6695
/**
6796
* A CounterOp describes an operation to be applied to a Counter object.
68-
* Spec: COP1
97+
* Spec: OCO1
6998
*/
70-
internal data class CounterOp(
99+
internal data class ObjectCounterOp(
71100
/**
72101
* The data value that should be added to the counter
73-
* Spec: COP2a
102+
* Spec: OCO2a
74103
*/
75-
val amount: Double
104+
val amount: Double? = null
76105
)
77106

78107
/**
79108
* A MapEntry represents the value at a given key in a Map object.
80109
* Spec: ME1
81110
*/
82-
internal data class MapEntry(
111+
internal data class ObjectMapEntry(
83112
/**
84113
* Indicates whether the map entry has been removed.
85-
* Spec: ME2a
114+
* Spec: OME2a
86115
*/
87116
val tombstone: Boolean? = null,
88117

89118
/**
90119
* The serial value of the last operation that was applied to the map entry.
91120
* It is optional in a MAP_CREATE operation and might be missing, in which case the client should use a nullish value for it
92121
* and treat it as the "earliest possible" serial for comparison purposes.
93-
* Spec: ME2b
122+
* Spec: OME2b
94123
*/
95124
val timeserial: String? = null,
96125

97126
/**
98127
* The data that represents the value of the map entry.
99-
* Spec: ME2c
128+
* Spec: OME2c
100129
*/
101130
val data: ObjectData? = null
102131
)
103132

104133
/**
105134
* An ObjectMap object represents a map of key-value pairs.
106-
* Spec: MAP1
135+
* Spec: OMP1
107136
*/
108137
internal data class ObjectMap(
109138
/**
110139
* The conflict-resolution semantics used by the map object.
111-
* Spec: MAP3a
140+
* Spec: OMP3a
112141
*/
113142
val semantics: MapSemantics? = null,
114143

115144
/**
116145
* The map entries, indexed by key.
117-
* Spec: MAP3b
146+
* Spec: OMP3b
118147
*/
119-
val entries: Map<String, MapEntry>? = null
148+
val entries: Map<String, ObjectMapEntry>? = null
120149
)
121150

122151
/**
123152
* An ObjectCounter object represents an incrementable and decrementable value
124-
* Spec: CNT1
153+
* Spec: OCN1
125154
*/
126155
internal data class ObjectCounter(
127156
/**
128157
* The value of the counter
129-
* Spec: CNT2a
158+
* Spec: OCN2a
130159
*/
131160
val count: Double? = null
132161
)
@@ -152,13 +181,13 @@ internal data class ObjectOperation(
152181
* The payload for the operation if it is an operation on a Map object type.
153182
* Spec: OOP3c
154183
*/
155-
val mapOp: MapOp? = null,
184+
val mapOp: ObjectMapOp? = null,
156185

157186
/**
158187
* The payload for the operation if it is an operation on a Counter object type.
159188
* Spec: OOP3d
160189
*/
161-
val counterOp: CounterOp? = null,
190+
val counterOp: ObjectCounterOp? = null,
162191

163192
/**
164193
* The payload for the operation if the operation is MAP_CREATE.
@@ -313,3 +342,114 @@ internal data class ObjectMessage(
313342
*/
314343
val siteCode: String? = null
315344
)
345+
346+
/**
347+
* Calculates the size of an ObjectMessage in bytes.
348+
* Spec: OM3
349+
*/
350+
internal fun ObjectMessage.size(): Int {
351+
val clientIdSize = clientId?.length ?: 0 // Spec: OM3f
352+
val operationSize = operation?.size() ?: 0 // Spec: OM3b, OOP4
353+
val objectStateSize = objectState?.size() ?: 0 // Spec: OM3c, OST3
354+
val extrasSize = extras?.let { gson.toJson(it).length } ?: 0 // Spec: OM3d
355+
356+
return clientIdSize + operationSize + objectStateSize + extrasSize
357+
}
358+
359+
/**
360+
* Calculates the size of an ObjectOperation in bytes.
361+
* Spec: OOP4
362+
*/
363+
private fun ObjectOperation.size(): Int {
364+
val mapOpSize = mapOp?.size() ?: 0 // Spec: OOP4b, OMO3
365+
val counterOpSize = counterOp?.size() ?: 0 // Spec: OOP4c, OCO3
366+
val mapSize = map?.size() ?: 0 // Spec: OOP4d, OMP4
367+
val counterSize = counter?.size() ?: 0 // Spec: OOP4e, OCN3
368+
369+
return mapOpSize + counterOpSize + mapSize + counterSize
370+
}
371+
372+
/**
373+
* Calculates the size of an ObjectState in bytes.
374+
* Spec: OST3
375+
*/
376+
private fun ObjectState.size(): Int {
377+
val mapSize = map?.size() ?: 0 // Spec: OST3b, OMP4
378+
val counterSize = counter?.size() ?: 0 // Spec: OST3c, OCN3
379+
val createOpSize = createOp?.size() ?: 0 // Spec: OST3d, OOP4
380+
381+
return mapSize + counterSize + createOpSize
382+
}
383+
384+
/**
385+
* Calculates the size of an ObjectMapOp in bytes.
386+
* Spec: OMO3
387+
*/
388+
private fun ObjectMapOp.size(): Int {
389+
val keySize = key.length // Spec: OMO3d - Size of the key
390+
val dataSize = data?.size() ?: 0 // Spec: OMO3b - Size of the data, calculated per "OD3"
391+
return keySize + dataSize
392+
}
393+
394+
/**
395+
* Calculates the size of a CounterOp in bytes.
396+
* Spec: OCO3
397+
*/
398+
private fun ObjectCounterOp.size(): Int {
399+
// Size is 8 if amount is a number, 0 if amount is null or omitted
400+
return if (amount != null) 8 else 0 // Spec: OCO3a, OCO3b
401+
}
402+
403+
/**
404+
* Calculates the size of an ObjectMap in bytes.
405+
* Spec: OMP4
406+
*/
407+
private fun ObjectMap.size(): Int {
408+
// Calculate the size of all map entries in the map property
409+
val entriesSize = entries?.entries?.sumOf {
410+
it.key.length + it.value.size() // // Spec: OMP4a1, OMP4a2
411+
} ?: 0
412+
413+
return entriesSize
414+
}
415+
416+
/**
417+
* Calculates the size of an ObjectCounter in bytes.
418+
* Spec: OCN3
419+
*/
420+
private fun ObjectCounter.size(): Int {
421+
// Size is 8 if count is a number, 0 if count is null or omitted
422+
return if (count != null) 8 else 0
423+
}
424+
425+
/**
426+
* Calculates the size of a MapEntry in bytes.
427+
* Spec: OME3
428+
*/
429+
private fun ObjectMapEntry.size(): Int {
430+
// The size is equal to the size of the data property, calculated per "OD3"
431+
return data?.size() ?: 0
432+
}
433+
434+
/**
435+
* Calculates the size of an ObjectData in bytes.
436+
* Spec: OD3
437+
*/
438+
private fun ObjectData.size(): Int {
439+
return value?.size() ?: 0 // Spec: OD3f
440+
}
441+
442+
/**
443+
* Calculates the size of an ObjectValue in bytes.
444+
* Spec: OD3*
445+
*/
446+
private fun ObjectValue.size(): Int {
447+
return when (value) {
448+
is Boolean -> 1 // Spec: OD3b
449+
is Binary -> value.size() // Spec: OD3c
450+
is Number -> 8 // Spec: OD3d
451+
is String -> value.byteSize // Spec: OD3e
452+
is JsonObject, is JsonArray -> value.toString().byteSize // Spec: OD3e
453+
else -> 0 // Spec: OD3f
454+
}
455+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.ably.lib.objects
2+
3+
import com.google.gson.Gson
4+
import com.google.gson.GsonBuilder
5+
6+
internal val gson: Gson = createGsonSerializer()
7+
8+
private fun createGsonSerializer(): Gson {
9+
return GsonBuilder().create() // Do not call serializeNulls() to omit null values
10+
}

0 commit comments

Comments
 (0)