Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A long queue of to-device messages can prevent outgoing federation working #17035

Open
shyrwall opened this issue Mar 27, 2024 · 21 comments
Open

Comments

@shyrwall
Copy link

Description

Hi

This will be a vague bug report so I'm hoping someone will come with a "aha" moment when reading this.

For the past week my homeserver has been unable to send outgoing federation messages towards matrix.org. Initially after starting synapse some messages go through but after a few seconds it stops and goes into a retry loop.
During these retries matrix.org , or rather cloudflare, returns a http error of 520.

Upon further inspection i managed to narrow it down to a m.direct_to_device (very large json object) being posted by a single user.
After deleting the event from device_federation_outbox everything worked again.

My theory is that the object was too large so matrix.org/cloudflare threw an error and Synapse just kept retrying.

If this is correct is there a bug in Synapse where it should somehow split this into multiple requests?
Or is it a matrix.org bug that has a low limit on requests size?

Attaching the deleted event.

Thank you
bad_edu.log

Steps to reproduce

Homeserver

xmr.se

Synapse Version

Multiple tested, 1.99 and up. Now 1.103.0

Installation Method

pip (from PyPI)

Database

postgresql. Single, no, no

Workers

Multiple workers

Platform

Not relevant.

Configuration

No response

Relevant log output

2024-03-25 19:22:26,936 - synapse.http.matrixfederationclient - 755 - INFO - federation_transaction_transmission_loop-24 - {PUT-O-29} [matrix.org] Got response headers: 520
2024-03-25 19:22:26,936 - synapse.http.matrixfederationclient - 798 - INFO - federation_transaction_transmission_loop-24 - {PUT-O-29} [matrix.org] Request failed: PUT matrix-federation://matrix.org/_matrix/federation/v1/send/1711389457952: HttpResponseException('520: ')

Anything else that would be useful to know?

No response

@S7evinK
Copy link
Contributor

S7evinK commented Jul 19, 2024

Did this happen again? Also, in the bad_edu.log, is 704458 the size of the EDU?
#17371 was merged recently, which had a similar symptom to the issue mentioned here. It may be that this is resolved by now.

@shyrwall
Copy link
Author

Did this happen again? Also, in the bad_edu.log, is 704458 the size of the EDU? #17371 was merged recently, which had a similar symptom to the issue mentioned here. It may be that this is resolved by now.

Sorry for the late reply. For some reason github emails have been going to my spam folder. It has not happened again. Let's assume it was fixed.

@shyrwall
Copy link
Author

Hi. I encountered the problem again in 1.113 and again with matrix.org . Couldn't log unfortunately but fixed by wiping the oldest edu which was ~600kb.

@shyrwall shyrwall reopened this Aug 30, 2024
@shyrwall
Copy link
Author

shyrwall commented Aug 30, 2024

Maybe not helpful information but now when it happened again i just checked the size of messages_json is device_federation_outbox and deleted all rows over 300kb. Had about 10 rows between 300-600kb. After deleting all queued messages to matrix.org were completed instantly. I have attached one of the big EDUs.

EDIT: Just realised now that I may be confused with PDU vs EDU :)

Uploading big_edu-240830.log…

@ansiwen
Copy link

ansiwen commented Sep 14, 2024

We also still see this issue (nitro.chat), see #17678 (comment)

The problem seems to be, that limiting the number of events in the EDUs to 50 is not enough, if there are huge events in it. Maybe we should just drop these big events, or are there valid reasons for them?

@shyrwall
Copy link
Author

We also still see this issue (nitro.chat), see #17678 (comment)

The problem seems to be, that limiting the number of events in the EDUs to 50 is not enough, if there are huge events in it. Maybe we should just drop these big events, or are there valid reasons for them?

Thanks for commenting. Will try your fix in #17678 next time it happens.

@ansiwen
Copy link

ansiwen commented Sep 14, 2024

We also still see this issue (nitro.chat), see #17678 (comment)
Thanks for commenting. Will try your fix in #17678 next time it happens.

It's not a fix, just a mitigation. It just further reduces the number of events per EDU from 50 to 25. I'm working on a real fix now.

@ansiwen
Copy link

ansiwen commented Sep 14, 2024

@shyrwall I'm now testing this patch, which uses the actual size of the strings in the JSON instead of a hardcoded 50 events. It stops adding events to a EDU when it's larger than 10k, it warns about events larger than 1k, and drops events completely they are larger than 100k. if it will work for a couple of days, I will submit it as PR:

EDIT: don't use this, use the patch below, which works.

diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py
index d097e65ea7..f46da8185d 100644
--- a/synapse/federation/sender/per_destination_queue.py
+++ b/synapse/federation/sender/per_destination_queue.py
@@ -71,7 +71,7 @@ CATCHUP_RETRY_INTERVAL = 60 * 60 * 1000
 
 # Limit how many presence states we add to each presence EDU, to ensure that
 # they are bounded in size.
-MAX_PRESENCE_STATES_PER_EDU = 50
+LIMIT_PRESENCE_SIZE_PER_EDU = 10000
 
 
 class PerDestinationQueue:
@@ -694,6 +694,16 @@ class PerDestinationQueue:
         self._pending_pdus = []
 
 
+def _json_strings_sum(obj):
+    if isinstance(obj, dict):
+        return sum(_json_strings_sum(key) + _json_strings_sum(value) for key, value in obj.items())
+    elif isinstance(obj, list):
+        return sum(_json_strings_sum(item) for item in obj)
+    elif isinstance(obj, str):
+        return len(obj)
+    else:
+        return 0
+
 @attr.s(slots=True, auto_attribs=True)
 class _TransactionQueueManager:
     """A helper async context manager for pulling stuff off the queues and
@@ -728,14 +738,21 @@ class _TransactionQueueManager:
             # Only send max 50 presence entries in the EDU, to bound the amount
             # of data we're sending.
             presence_to_add: List[JsonDict] = []
+            edu_size = 0
             while (
                 self.queue._pending_presence
-                and len(presence_to_add) < MAX_PRESENCE_STATES_PER_EDU
+                and edu_size < LIMIT_PRESENCE_SIZE_PER_EDU
             ):
                 _, presence = self.queue._pending_presence.popitem(last=False)
-                presence_to_add.append(
-                    format_user_presence_state(presence, self.queue._clock.time_msec())
-                )
+                formatted_presence = format_user_presence_state(presence, self.queue._clock.time_msec())
+                presence_size = _json_strings_sum(formatted_presence)
+                if presence_size > 1000:
+                    logger.warning("Large presence event (size: %d): %s", presence_size, str(formatted_presence)[:5000])
+                    if presence_size > 100000:
+                        logger.warning("Dropping huge presence event")
+                        continue
+                edu_size += presence_size
+                presence_to_add.append(formatted_presence)
 
             pending_edus.append(
                 Edu(

@shyrwall
Copy link
Author

That sounds much better. I was not familiar with how it actually worked. That one edu could contain multiple events etc. I guess there is no way of knowing if the patch works if I implement it after the "error" has occured because then it's already in the device federation outbox . But I will implement it and hope the problem does not occur again :)

@ansiwen
Copy link

ansiwen commented Sep 19, 2024

my last patch didn't help us. as in your case, @shyrwall, the large EDUs are "to device messages", which are not presence EDUs, but handled separately in later code. Here is the new patch, that I'm testing currently. It has a cut-off if the to-device EDUs in one transaction are getting using more than 100kB, and completely drops huge to-device EDUs (larger than 500kB):

diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py
index d097e65ea7..953a1067b6 100644
--- a/synapse/federation/sender/per_destination_queue.py
+++ b/synapse/federation/sender/per_destination_queue.py
@@ -73,6 +73,15 @@ CATCHUP_RETRY_INTERVAL = 60 * 60 * 1000
 # they are bounded in size.
 MAX_PRESENCE_STATES_PER_EDU = 50
 
+def _strings_sum(obj):
+    if isinstance(obj, dict):
+        return sum(_strings_sum(key) + _strings_sum(value) for key, value in obj.items())
+    elif isinstance(obj, list):
+        return sum(_strings_sum(item) for item in obj)
+    elif isinstance(obj, str):
+        return len(obj)
+    else:
+        return 0
 
 class PerDestinationQueue:
     """
@@ -654,15 +663,26 @@ class PerDestinationQueue:
     async def _get_to_device_message_edus(self, limit: int) -> Tuple[List[Edu], int]:
         last_device_stream_id = self._last_device_stream_id
         to_device_stream_id = self._store.get_to_device_stream_token()
-        contents, stream_id = await self._store.get_new_device_msgs_for_remote(
+        messages, stream_id = await self._store.get_new_device_msgs_for_remote(
             self._destination, last_device_stream_id, to_device_stream_id, limit
         )
-        for content in contents:
-            message_id = content.get("message_id")
-            if not message_id:
+        total_size = 0
+        contents: List[JsonDict] = []
+        for sid, content in messages:
+            size = _strings_sum(content)
+            logger.info("to-device-edu-size: %d", size)
+            if size > 500000:
+                logger.warning("Dropping huge to-device-message: %s", str(content)[:5000])
                 continue
-
-            set_tag(SynapseTags.TO_DEVICE_EDU_ID, message_id)
+            message_id = content.get("message_id")
+            if message_id:
+                set_tag(SynapseTags.TO_DEVICE_EDU_ID, message_id)
+            total_size += size
+            contents.append(content)
+            if total_size > 100000:
+                logger.warning("to-device messages are too large (size: %d), defer rest to next transaction.", total_size)
+                stream_id = sid
+                break
 
         edus = [
             Edu(
diff --git a/synapse/storage/databases/main/deviceinbox.py b/synapse/storage/databases/main/deviceinbox.py
index 0612b82b9b..e45110c4ed 100644
--- a/synapse/storage/databases/main/deviceinbox.py
+++ b/synapse/storage/databases/main/deviceinbox.py
@@ -569,7 +569,7 @@ class DeviceInboxWorkerStore(SQLBaseStore):
     @trace
     async def get_new_device_msgs_for_remote(
         self, destination: str, last_stream_id: int, current_stream_id: int, limit: int
-    ) -> Tuple[List[JsonDict], int]:
+    ) -> Tuple[List[Tuple[int, JsonDict]], int]:
         """
         Args:
             destination: The name of the remote server.
@@ -599,7 +599,7 @@ class DeviceInboxWorkerStore(SQLBaseStore):
         @trace
         def get_new_messages_for_remote_destination_txn(
             txn: LoggingTransaction,
-        ) -> Tuple[List[JsonDict], int]:
+        ) -> Tuple[List[Tuple[int, JsonDict]], int]:
             sql = (
                 "SELECT stream_id, messages_json FROM device_federation_outbox"
                 " WHERE destination = ?"
@@ -609,12 +609,12 @@ class DeviceInboxWorkerStore(SQLBaseStore):
             )
             txn.execute(sql, (destination, last_stream_id, current_stream_id, limit))
 
-            messages = []
+            messages: List[Tuple[int, JsonDict]] = []
             stream_pos = current_stream_id
 
             for row in txn:
                 stream_pos = row[0]
-                messages.append(db_to_json(row[1]))
+                messages.append((stream_pos, db_to_json(row[1])))
 
             # If the limit was not reached we know that there's no more data for this
             # user/device pair up to current_stream_id.

@wj25czxj47bu6q
Copy link

wj25czxj47bu6q commented Nov 12, 2024

@ansiwen Thank you so much for this patch. ArcticFoxes.net has been experiencing inexplicable outbound federation issues with Matrix.org for many months until someone brought this issue to my attention, and your (second) patch finally helped solve the problem.


For context for anyone else, look for entries like these in the log:

2024-11-12 17:43:50,619 - synapse.federation.sender.transaction_manager - 127 - INFO - federation_transaction_transmission_loop-126169 - TX [matrix.org] {1731390077364} Sending transaction [
1731390077364], (PDUs: 50, EDUs: 100)
2024-11-12 17:43:51,440 - synapse.http.matrixfederationclient - 767 - INFO - federation_transaction_transmission_loop-126169 - {PUT-O-194302} [matrix.org] Got response headers: 520 
2024-11-12 17:43:51,441 - synapse.http.matrixfederationclient - 810 - INFO - federation_transaction_transmission_loop-126169 - {PUT-O-194302} [matrix.org] Request failed: PUT matrix-federation://matrix.org/_matrix/federation/v1/send/1731390077364: HttpResponseException('520: ')

Cloudflare returns error code 520 due to some sort of issue with the backend server. @turt2live confirmed back in May 2024 that our federation requests were failing at the Matrix.org reverse proxy:

image

Through this GitHub issue, we determined that the reverse proxy for Matrix.org was silently dropping the requests because they were too large.1 At some point the EDUs in a single transaction grow too large (possibly due to a single excessively large EDU, not sure), so Matrix.org drops the request. Since the request is unsuccessful, the failed transaction and successive transactions pile up in the Synapse queue until the maximum of 50 PDUs and 100 EDUs per transaction is reached. Another symptom of this issue is that federation to Matrix.org works properly for a brief period immediately after restarting Synapse.

Footnotes

  1. It is still possible that Synapse was generating requests that were malformed in some way, but that seems increasingly unlikely given all the information now available.

@Titaniumtown
Copy link

This is currently also affecting envs.net, completely preventing any envs.net -> matrix.org communication. Although this doesn't affect viewing content originating from matrix.org.

@sarah4d2
Copy link

sarah4d2 commented Dec 3, 2024

We've experienced this on 4d2.org as well, with similar symptoms as described, including federation working very briefly after restarting Synapse - or in our case, the specific federation-sender worker queueing messages for matrix.org.

Deleting large messages from device_federation_outbox temporarily fixes the problem, at the cost of some data loss:

delete from device_federation_outbox where destination = 'matrix.org' and length(messages_json) > 500000;

@richvdh
Copy link
Member

richvdh commented Dec 10, 2024

Just digging into this a bit: matrix-rust-sdk sends to-device messages in batches of up to 250 target devices at a time. Each individual to-device message can be up to about 2500 bytes. The batch is then divided up into target homeservers, and added to device_federation_outbox. If all 250 target devices are on the same target homeserver, that makes a 625000-byte batch of to-device messages, which ends up as a single m.direct_to_device EDU in a /_matrix/federation/v1/send request.

Synapse then puts up to 100 of those EDUs in a single /_matrix/federation/v1/send request (alongside any PDUs which are ready to be sent). So, you can get a 60MiB federation transaction with fairly normal behaviour, which is well above the maximum request size Synapse will accept (which is about 12.5MiB)

Synapse immediately closes the TCP connection when a request exceeds the limit; Cloudflare therefore sees an empty HTTP response, which it turns into a 520 error.

@TheArcaneBrony
Copy link

I wonder if it might be worthwhile to spec a MESSAGE_TOO_LARGE errorcode on federation endpoints so homeservers can indicate how large of a message they are willing to accept? Just throwing ideas in the ring here, though. 60 MiB doesn't sound all too dramatic to me, but i'm sure others would be happy to disagree.

@richvdh richvdh changed the title Strange federation bug. Possibly in Synapse , matrix.org or both. A long queue of to-device messages can prevent outgoing federation working Dec 10, 2024
@richvdh
Copy link
Member

richvdh commented Dec 10, 2024

@TheArcaneBrony I would think setting an agreed maximum length that senders have to stay within and receivers have to accept would be a much simpler solution than some sort of negotiation system.

@ansiwen
Copy link

ansiwen commented Dec 10, 2024

@TheArcaneBrony I would think setting an agreed maximum length that senders have to stay within and receivers have to accept would be a much simpler solution than some sort of negotiation system.

I agree in general, but the maximum has to be chosen wisely, because it also implicitly limits the maximum size of a single EDU (or is there already a mechanism to fragment large EDUs?). Your example from above indicates there are "valid" 625kB EDUs. However, in your example it could be easily fixed by creating smaller batches.

@richvdh
Copy link
Member

richvdh commented Dec 10, 2024

Related: matrix-org/matrix-spec#807

@ansiwen
Copy link

ansiwen commented Jan 3, 2025

After a long quiet time it happened to us again (the patch was not applied anymore because of automatic updates), and now over the holidays it happened again. I could collect some debug logging and it was a burst of large to-device EDUs of size around 500kB each, which resulted in a total transaction size of 32MB which appears to be blocked by cloudflare.

Click here to show log
2025-01-03 15:26:40,093 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 475434
2025-01-03 15:26:40,094 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 543747
2025-01-03 15:26:40,095 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 543163
2025-01-03 15:26:40,097 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 534078
2025-01-03 15:26:40,098 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 471609
2025-01-03 15:26:40,099 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 519021
2025-01-03 15:26:40,100 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 470068
2025-01-03 15:26:40,101 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 532778
2025-01-03 15:26:40,102 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 552020
2025-01-03 15:26:40,104 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 527233
2025-01-03 15:26:40,105 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 537778
2025-01-03 15:26:40,106 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 478798
2025-01-03 15:26:40,107 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 540220
2025-01-03 15:26:40,108 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 532638
2025-01-03 15:26:40,109 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 544111
2025-01-03 15:26:40,110 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 555493
2025-01-03 15:26:40,111 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 456633
2025-01-03 15:26:40,112 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 552253
2025-01-03 15:26:40,113 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 530760
2025-01-03 15:26:40,114 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 560122
2025-01-03 15:26:40,115 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 537784
2025-01-03 15:26:40,116 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 508617
2025-01-03 15:26:40,116 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 463378
2025-01-03 15:26:40,117 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 540551
2025-01-03 15:26:40,118 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 538920
2025-01-03 15:26:40,119 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 555271
2025-01-03 15:26:40,119 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 544032
2025-01-03 15:26:40,120 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 547213
2025-01-03 15:26:40,121 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 551611
2025-01-03 15:26:40,122 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 544832
2025-01-03 15:26:40,122 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 552963
2025-01-03 15:26:40,123 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 367734
2025-01-03 15:26:40,124 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 519363
2025-01-03 15:26:40,124 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 425897
2025-01-03 15:26:40,125 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 554086
2025-01-03 15:26:40,126 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 542999
2025-01-03 15:26:40,126 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 543890
2025-01-03 15:26:40,127 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 532574
2025-01-03 15:26:40,128 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 341170
2025-01-03 15:26:40,128 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 551913
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 552697
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 24045
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 2434
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 3829
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 12817
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 12938
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 2620
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 7642
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 6651
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 4113
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 2790
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 1296
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 2789
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 9289
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 14448
2025-01-03 15:26:40,129 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 9142
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 4112
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 19524
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 11605
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 9090
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 11765
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 15329
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 18656
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 5416
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 2763
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 27069
2025-01-03 15:26:40,130 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 4675
2025-01-03 15:26:40,131 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 504892
2025-01-03 15:26:40,131 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 556967
2025-01-03 15:26:40,132 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 550078
2025-01-03 15:26:40,133 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 566756
2025-01-03 15:26:40,133 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 554399
2025-01-03 15:26:40,134 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 564696
2025-01-03 15:26:40,135 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 554548
2025-01-03 15:26:40,135 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 542514
2025-01-03 15:26:40,136 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 526662
2025-01-03 15:26:40,137 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 463566
2025-01-03 15:26:40,137 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 483464
2025-01-03 15:26:40,138 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 453992
2025-01-03 15:26:40,139 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 550914
2025-01-03 15:26:40,139 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 523115
2025-01-03 15:26:40,140 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 539298
2025-01-03 15:26:40,140 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 530257
2025-01-03 15:26:40,141 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 534887
2025-01-03 15:26:40,142 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 338915
2025-01-03 15:26:40,142 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 565373
2025-01-03 15:26:40,143 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 518341
2025-01-03 15:26:40,144 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 551611
2025-01-03 15:26:40,144 - synapse.federation.sender.per_destination_queue - 661 - INFO - federation_transaction_transmission_loop-87 - to-device-edu-size: 226948
2025-01-03 15:26:40,144 - synapse.federation.sender.per_destination_queue - 669 - WARNING - federation_transaction_transmission_loop-87 - to-device messages are large: 32724492

I still don't know where the limit is, but for now I changed my patch from above to drop to-device EDUs that are larger than 1MB, and also have a threshold of 1MB per transaction, which seems to be able to handle all EDUs I am observing without stalling.

Here the updated patch for matrix-synapse 1.121.1
diff --git a/synapse/federation/sender/per_destination_queue.py b/synapse/federation/sender/per_destination_queue.py
index d097e65ea7..953a1067b6 100644
--- a/synapse/federation/sender/per_destination_queue.py
+++ b/synapse/federation/sender/per_destination_queue.py
@@ -73,6 +73,15 @@ CATCHUP_RETRY_INTERVAL = 60 * 60 * 1000
 # they are bounded in size.
 MAX_PRESENCE_STATES_PER_EDU = 50
 
+def _strings_sum(obj):
+    if isinstance(obj, dict):
+        return sum(_strings_sum(key) + _strings_sum(value) for key, value in obj.items())
+    elif isinstance(obj, list):
+        return sum(_strings_sum(item) for item in obj)
+    elif isinstance(obj, str):
+        return len(obj)
+    else:
+        return 0
 
 class PerDestinationQueue:
     """
@@ -642,15 +651,26 @@ class PerDestinationQueue:
     async def _get_to_device_message_edus(self, limit: int) -> Tuple[List[Edu], int]:
         last_device_stream_id = self._last_device_stream_id
         to_device_stream_id = self._store.get_to_device_stream_token()
-        contents, stream_id = await self._store.get_new_device_msgs_for_remote(
+        messages, stream_id = await self._store.get_new_device_msgs_for_remote(
             self._destination, last_device_stream_id, to_device_stream_id, limit
         )
-        for content in contents:
-            message_id = content.get("message_id")
-            if not message_id:
+        total_size = 0
+        contents: List[JsonDict] = []
+        for sid, content in messages:
+            size = _strings_sum(content)
+            logger.info("to-device-edu-size: %d", size)
+            if size > 1000000:
+                logger.warning("Dropping huge to-device-message: %s", str(content)[:5000])
                 continue
-
-            set_tag(SynapseTags.TO_DEVICE_EDU_ID, message_id)
+            message_id = content.get("message_id")
+            if message_id:
+                set_tag(SynapseTags.TO_DEVICE_EDU_ID, message_id)
+            total_size += size
+            contents.append(content)
+            if total_size > 1000000:
+                logger.warning("to-device messages are too large (size: %d), defer rest to next transaction.", total_size)
+                stream_id = sid
+                break
 
         edus = [
             Edu(
diff --git a/synapse/storage/databases/main/deviceinbox.py b/synapse/storage/databases/main/deviceinbox.py
index 0612b82b9b..e45110c4ed 100644
--- a/synapse/storage/databases/main/deviceinbox.py
+++ b/synapse/storage/databases/main/deviceinbox.py
@@ -569,7 +569,7 @@ class DeviceInboxWorkerStore(SQLBaseStore):
     @trace
     async def get_new_device_msgs_for_remote(
         self, destination: str, last_stream_id: int, current_stream_id: int, limit: int
-    ) -> Tuple[List[JsonDict], int]:
+    ) -> Tuple[List[Tuple[int, JsonDict]], int]:
         """
         Args:
             destination: The name of the remote server.
@@ -599,7 +599,7 @@ class DeviceInboxWorkerStore(SQLBaseStore):
         @trace
         def get_new_messages_for_remote_destination_txn(
             txn: LoggingTransaction,
-        ) -> Tuple[List[JsonDict], int]:
+        ) -> Tuple[List[Tuple[int, JsonDict]], int]:
             sql = (
                 "SELECT stream_id, messages_json FROM device_federation_outbox"
                 " WHERE destination = ?"
@@ -609,12 +609,12 @@ class DeviceInboxWorkerStore(SQLBaseStore):
             )
             txn.execute(sql, (destination, last_stream_id, current_stream_id, limit))
 
-            messages = []
+            messages: List[Tuple[int, JsonDict]] = []
             stream_pos = current_stream_id
 
             for row in txn:
                 stream_pos = row[0]
-                messages.append(db_to_json(row[1]))
+                messages.append((stream_pos, db_to_json(row[1])))
 
             # If the limit was not reached we know that there's no more data for this
             # user/device pair up to current_stream_id.

@lunarthegrey
Copy link

We've experienced this on 4d2.org as well, with similar symptoms as described, including federation working very briefly after restarting Synapse - or in our case, the specific federation-sender worker queueing messages for matrix.org.

Deleting large messages from device_federation_outbox temporarily fixes the problem, at the cost of some data loss:

delete from device_federation_outbox where destination = 'matrix.org' and length(messages_json) > 500000;

We recently had the same issue at https://unredacted.org/ and that query resolved the issue.

@ansiwen
Copy link

ansiwen commented Jan 7, 2025

We recently had the same issue at https://unredacted.org/ and that query resolved the issue.

@lunarthegrey FYI: If you apply my patch and restart the federation sender it should be resolved without data loss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants