Skip to content

Commit 0549307

Browse files
authored
Revert "Limit outgoing to_device EDU size to 65536" (#19614)
Reverts #18416 Unfortunately, this causes failures on `/sendToDevice` endpoint in normal circumstances. If a single user has, say, a hundred devices then we easily go over the limit. This blocks message sending entirely in encrypted rooms. cc @MadLittleMods @MatMaul
1 parent 539f708 commit 0549307

7 files changed

Lines changed: 38 additions & 360 deletions

File tree

changelog.d/18416.bugfix

Lines changed: 0 additions & 1 deletion
This file was deleted.

synapse/api/constants.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,6 @@
3030

3131
# the max size of a (canonical-json-encoded) event
3232
MAX_PDU_SIZE = 65536
33-
# This isn't spec'ed but is our own reasonable default to play nice with Synapse's
34-
# `max_request_size`/`max_request_body_size`. We chose the same as `MAX_PDU_SIZE` as our
35-
# `max_request_body_size` math is currently limited by 200 `MAX_PDU_SIZE` things. The
36-
# spec for a `/federation/v1/send` request sets the limit at 100 EDU's and 50 PDU's
37-
# which is below that 200 `MAX_PDU_SIZE` limit (`max_request_body_size`).
38-
#
39-
# Allowing oversized EDU's results in failed `/federation/v1/send` transactions (because
40-
# the request overall can overrun the `max_request_body_size`) which are retried over
41-
# and over and prevent other outbound federation traffic from happening.
42-
MAX_EDU_SIZE = 65536
43-
44-
# This is defined in the Matrix spec and enforced by the receiver.
45-
MAX_EDUS_PER_TRANSACTION = 100
46-
# A transaction can contain up to 100 EDUs but synapse reserves 10 EDUs for other purposes
47-
# like trickling out some device list updates.
48-
NUMBER_OF_RESERVED_EDUS_PER_TRANSACTION = 10
4933

5034
# The maximum allowed size of an HTTP request.
5135
# Other than media uploads, the biggest request we expect to see is a fully-loaded

synapse/federation/sender/per_destination_queue.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@
3030

3131
from twisted.internet import defer
3232

33-
from synapse.api.constants import (
34-
MAX_EDUS_PER_TRANSACTION,
35-
NUMBER_OF_RESERVED_EDUS_PER_TRANSACTION,
36-
EduTypes,
37-
)
33+
from synapse.api.constants import EduTypes
3834
from synapse.api.errors import (
3935
FederationDeniedError,
4036
HttpResponseException,
@@ -55,6 +51,9 @@
5551
if TYPE_CHECKING:
5652
import synapse.server
5753

54+
# This is defined in the Matrix spec and enforced by the receiver.
55+
MAX_EDUS_PER_TRANSACTION = 100
56+
5857
logger = logging.getLogger(__name__)
5958

6059

@@ -799,9 +798,7 @@ async def __aenter__(self) -> tuple[list[EventBase], list[Edu]]:
799798
(
800799
to_device_edus,
801800
device_stream_id,
802-
) = await self.queue._get_to_device_message_edus(
803-
edu_limit - NUMBER_OF_RESERVED_EDUS_PER_TRANSACTION
804-
)
801+
) = await self.queue._get_to_device_message_edus(edu_limit - 10)
805802

806803
if to_device_edus:
807804
self._device_stream_id = device_stream_id

synapse/handlers/devicemessage.py

Lines changed: 23 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,8 @@
2323
from http import HTTPStatus
2424
from typing import TYPE_CHECKING, Any
2525

26-
from canonicaljson import encode_canonical_json
27-
28-
from synapse.api.constants import (
29-
MAX_EDU_SIZE,
30-
EduTypes,
31-
EventContentFields,
32-
ToDeviceEventTypes,
33-
)
34-
from synapse.api.errors import Codes, EventSizeError, SynapseError
26+
from synapse.api.constants import EduTypes, EventContentFields, ToDeviceEventTypes
27+
from synapse.api.errors import Codes, SynapseError
3528
from synapse.api.ratelimiting import Ratelimiter
3629
from synapse.logging.context import run_in_background
3730
from synapse.logging.opentracing import (
@@ -42,7 +35,7 @@
4235
)
4336
from synapse.types import JsonDict, Requester, StreamKeyType, UserID, get_domain_from_id
4437
from synapse.util.json import json_encoder
45-
from synapse.util.stringutils import random_string_insecure_fast
38+
from synapse.util.stringutils import random_string
4639

4740
if TYPE_CHECKING:
4841
from synapse.server import HomeServer
@@ -229,7 +222,6 @@ async def send_device_message(
229222
set_tag(SynapseTags.TO_DEVICE_TYPE, message_type)
230223
set_tag(SynapseTags.TO_DEVICE_SENDER, sender_user_id)
231224
local_messages = {}
232-
# Map from destination (server) -> recipient (user ID) -> device_id -> JSON message content
233225
remote_messages: dict[str, dict[str, dict[str, JsonDict]]] = {}
234226
for user_id, by_device in messages.items():
235227
if not UserID.is_valid(user_id):
@@ -285,33 +277,28 @@ async def send_device_message(
285277
destination = get_domain_from_id(user_id)
286278
remote_messages.setdefault(destination, {})[user_id] = by_device
287279

288-
# Add local messages to the database.
280+
context = get_active_span_text_map()
281+
282+
remote_edu_contents = {}
283+
for destination, messages in remote_messages.items():
284+
# The EDU contains a "message_id" property which is used for
285+
# idempotence. Make up a random one.
286+
message_id = random_string(16)
287+
log_kv({"destination": destination, "message_id": message_id})
288+
289+
remote_edu_contents[destination] = {
290+
"messages": messages,
291+
"sender": sender_user_id,
292+
"type": message_type,
293+
"message_id": message_id,
294+
"org.matrix.opentracing_context": json_encoder.encode(context),
295+
}
296+
297+
# Add messages to the database.
289298
# Retrieve the stream id of the last-processed to-device message.
290-
last_stream_id = (
291-
await self.store.add_local_messages_from_client_to_device_inbox(
292-
local_messages
293-
)
299+
last_stream_id = await self.store.add_messages_to_device_inbox(
300+
local_messages, remote_edu_contents
294301
)
295-
for destination, messages in remote_messages.items():
296-
split_edus = split_device_messages_into_edus(
297-
sender_user_id, message_type, messages
298-
)
299-
for edu in split_edus:
300-
edu["org.matrix.opentracing_context"] = json_encoder.encode(
301-
get_active_span_text_map()
302-
)
303-
# Add remote messages to the database.
304-
last_stream_id = (
305-
await self.store.add_remote_messages_from_client_to_device_inbox(
306-
{destination: edu}
307-
)
308-
)
309-
log_kv(
310-
{
311-
"destination": destination,
312-
"message_id": edu["message_id"],
313-
}
314-
)
315302

316303
# Notify listeners that there are new to-device messages to process,
317304
# handing them the latest stream id.
@@ -410,102 +397,3 @@ async def get_events_for_dehydrated_device(
410397
"events": messages,
411398
"next_batch": f"d{stream_id}",
412399
}
413-
414-
415-
def split_device_messages_into_edus(
416-
sender_user_id: str,
417-
message_type: str,
418-
messages_by_user_then_device: dict[str, dict[str, JsonDict]],
419-
) -> list[JsonDict]:
420-
"""
421-
This function takes many to-device messages and fits/splits them into several EDUs
422-
as necessary. We split the messages up as the overall request can overrun the
423-
`max_request_body_size` and prevent outbound federation traffic because of the size
424-
of the transaction (cf. `MAX_EDU_SIZE`).
425-
426-
Args:
427-
sender_user_id: The user that is sending the to-device messages.
428-
message_type: The type of to-device messages that are being sent.
429-
messages_by_user_then_device: Dictionary of recipient user_id to recipient device_id to message.
430-
431-
Returns:
432-
Bin-packed list of EDU JSON content for the given to_device messages
433-
434-
Raises:
435-
EventSizeError: If a single to-device message is too large to fit into an EDU.
436-
"""
437-
split_edus_content: list[JsonDict] = []
438-
439-
# Convert messages dict to a list of (recipient, messages_by_device) pairs
440-
message_items = list(messages_by_user_then_device.items())
441-
442-
while message_items:
443-
edu_messages = {}
444-
# Start by trying to fit all remaining messages
445-
target_count = len(message_items)
446-
447-
while target_count > 0:
448-
# Take the first target_count messages
449-
edu_messages = dict(message_items[:target_count])
450-
edu_content = create_new_to_device_edu_content(
451-
sender_user_id, message_type, edu_messages
452-
)
453-
# Let's add the whole EDU structure before testing the size
454-
edu = {
455-
"content": edu_content,
456-
"edu_type": EduTypes.DIRECT_TO_DEVICE,
457-
}
458-
459-
if len(encode_canonical_json(edu)) <= MAX_EDU_SIZE:
460-
# It fits! Add this EDU and remove these messages from the list
461-
split_edus_content.append(edu_content)
462-
message_items = message_items[target_count:]
463-
464-
logger.debug(
465-
"Created EDU with %d recipients from %s (message_id=%s), (total EDUs so far: %d)",
466-
target_count,
467-
sender_user_id,
468-
edu_content["message_id"],
469-
len(split_edus_content),
470-
)
471-
break
472-
else:
473-
if target_count == 1:
474-
# Single recipient's messages are too large, let's reject the client
475-
# call with 413/`M_TOO_LARGE`, we expect this error to reach the
476-
# client in the case of the /sendToDevice endpoint.
477-
#
478-
# 413 is currently an unspecced response for `/sendToDevice` but is
479-
# probably the best thing we can do.
480-
# https://github.com/matrix-org/matrix-spec/pull/2340 tracks adding
481-
# this to the spec
482-
recipient = message_items[0][0]
483-
raise EventSizeError(
484-
f"device message to {recipient} too large to fit in a single EDU",
485-
unpersistable=True,
486-
)
487-
488-
# Halve the number of messages and try again
489-
target_count = target_count // 2
490-
491-
return split_edus_content
492-
493-
494-
def create_new_to_device_edu_content(
495-
sender_user_id: str,
496-
message_type: str,
497-
messages_by_user_then_device: dict[str, dict[str, JsonDict]],
498-
) -> JsonDict:
499-
"""
500-
Create a new `m.direct_to_device` EDU `content` object with a unique message ID.
501-
"""
502-
# The EDU contains a "message_id" property which is used for
503-
# idempotence. Make up a random one.
504-
message_id = random_string_insecure_fast(16)
505-
content = {
506-
"sender": sender_user_id,
507-
"type": message_type,
508-
"message_id": message_id,
509-
"messages": messages_by_user_then_device,
510-
}
511-
return content

synapse/storage/databases/main/deviceinbox.py

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -739,15 +739,18 @@ def get_all_new_device_messages_txn(
739739
)
740740

741741
@trace
742-
async def add_local_messages_from_client_to_device_inbox(
742+
async def add_messages_to_device_inbox(
743743
self,
744744
local_messages_by_user_then_device: dict[str, dict[str, JsonDict]],
745+
remote_messages_by_destination: dict[str, JsonDict],
745746
) -> int:
746-
"""Queue local device messages that will be sent to devices of local users.
747+
"""Used to send messages from this server.
747748
748749
Args:
749750
local_messages_by_user_then_device:
750751
Dictionary of recipient user_id to recipient device_id to message.
752+
remote_messages_by_destination:
753+
Dictionary of destination server_name to the EDU JSON to send.
751754
752755
Returns:
753756
The new stream_id.
@@ -763,39 +766,6 @@ def add_messages_txn(
763766
txn, stream_id, local_messages_by_user_then_device
764767
)
765768

766-
async with self._to_device_msg_id_gen.get_next() as stream_id:
767-
now_ms = self.clock.time_msec()
768-
await self.db_pool.runInteraction(
769-
"add_local_messages_from_client_to_device_inbox",
770-
add_messages_txn,
771-
now_ms,
772-
stream_id,
773-
)
774-
for user_id in local_messages_by_user_then_device.keys():
775-
self._device_inbox_stream_cache.entity_has_changed(user_id, stream_id)
776-
777-
return self._to_device_msg_id_gen.get_current_token()
778-
779-
@trace
780-
async def add_remote_messages_from_client_to_device_inbox(
781-
self,
782-
remote_messages_by_destination: dict[str, JsonDict],
783-
) -> int:
784-
"""Queue device messages that will be sent to remote servers.
785-
786-
Args:
787-
remote_messages_by_destination:
788-
Dictionary of destination server_name to the EDU JSON to send.
789-
790-
Returns:
791-
The new stream_id.
792-
"""
793-
794-
assert self._can_write_to_device
795-
796-
def add_messages_txn(
797-
txn: LoggingTransaction, now_ms: int, stream_id: int
798-
) -> None:
799769
# Add the remote messages to the federation outbox.
800770
# We'll send them to a remote server when we next send a
801771
# federation transaction to that destination.
@@ -854,11 +824,10 @@ def add_messages_txn(
854824
async with self._to_device_msg_id_gen.get_next() as stream_id:
855825
now_ms = self.clock.time_msec()
856826
await self.db_pool.runInteraction(
857-
"add_remote_messages_from_client_to_device_inbox",
858-
add_messages_txn,
859-
now_ms,
860-
stream_id,
827+
"add_messages_to_device_inbox", add_messages_txn, now_ms, stream_id
861828
)
829+
for user_id in local_messages_by_user_then_device.keys():
830+
self._device_inbox_stream_cache.entity_has_changed(user_id, stream_id)
862831
for destination in remote_messages_by_destination.keys():
863832
self._device_federation_outbox_stream_cache.entity_has_changed(
864833
destination, stream_id

tests/handlers/test_appservice.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -856,9 +856,7 @@ def test_application_services_receive_bursts_of_to_device(self) -> None:
856856

857857
# Seed the device_inbox table with our fake messages
858858
self.get_success(
859-
self.hs.get_datastores().main.add_local_messages_from_client_to_device_inbox(
860-
messages
861-
)
859+
self.hs.get_datastores().main.add_messages_to_device_inbox(messages, {})
862860
)
863861

864862
# Now have local_user send a final to-device message to exclusive_as_user. All unsent

0 commit comments

Comments
 (0)