Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit c071cd5

Browse files
author
David Robertson
authored
Ensure fed-sender catchup does not block for full state (#15248)
* Reproduce bad scenario in test * Avoid catchup optimisation for partial state rooms
1 parent 4bb26c9 commit c071cd5

File tree

4 files changed

+125
-3
lines changed

4 files changed

+125
-3
lines changed

changelog.d/15248.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a rare bug introduced in Synapse 1.73 where events could remain unsent to other homeservers after a faster-join to a room.

synapse/federation/sender/per_destination_queue.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,8 +497,8 @@ async def _catch_up_transmission_loop(self) -> None:
497497
#
498498
# Note: `catchup_pdus` will have exactly one PDU per room.
499499
for pdu in catchup_pdus:
500-
# The PDU from the DB will be the last PDU in the room from
501-
# *this server* that wasn't sent to the remote. However, other
500+
# The PDU from the DB will be the newest PDU in the room from
501+
# *this server* that we tried---but were unable---to send to the remote.
502502
# servers may have sent lots of events since then, and we want
503503
# to try and tell the remote only about the *latest* events in
504504
# the room. This is so that it doesn't get inundated by events
@@ -516,6 +516,11 @@ async def _catch_up_transmission_loop(self) -> None:
516516
# If the event is in the extremities, then great! We can just
517517
# use that without having to do further checks.
518518
room_catchup_pdus = [pdu]
519+
elif await self._store.is_partial_state_room(pdu.room_id):
520+
# We can't be sure which events the destination should
521+
# see using only partial state. Avoid doing so, and just retry
522+
# sending our the newest PDU the remote is missing from us.
523+
room_catchup_pdus = [pdu]
519524
else:
520525
# If not, fetch the extremities and figure out which we can
521526
# send.

tests/federation/test_federation_catch_up.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Callable, List, Optional, Tuple
1+
from typing import Callable, Collection, List, Optional, Tuple
2+
from unittest import mock
23
from unittest.mock import Mock
34

45
from twisted.test.proto_helpers import MemoryReactor
@@ -500,3 +501,87 @@ def test_not_latest_event(self) -> None:
500501
self.assertEqual(len(sent_pdus), 1)
501502
self.assertEqual(sent_pdus[0].event_id, event_2.event_id)
502503
self.assertFalse(per_dest_queue._catching_up)
504+
505+
def test_catch_up_is_not_blocked_by_remote_event_in_partial_state_room(
506+
self,
507+
) -> None:
508+
"""Detects (part of?) https://github.com/matrix-org/synapse/issues/15220."""
509+
# ARRANGE:
510+
# - a local user (u1)
511+
# - a room which contains u1 and two remote users, @u2:host2 and @u3:other
512+
# - events in that room such that
513+
# - history visibility is restricted
514+
# - u1 sent message events e1 and e2
515+
# - afterwards, u3 sent a remote event e3
516+
# - catchup to begin for host2; last successfully sent event was e1
517+
per_dest_queue, sent_pdus = self.make_fake_destination_queue()
518+
519+
self.register_user("u1", "you the one")
520+
u1_token = self.login("u1", "you the one")
521+
room = self.helper.create_room_as("u1", tok=u1_token)
522+
self.helper.send_state(
523+
room_id=room,
524+
event_type="m.room.history_visibility",
525+
body={"history_visibility": "joined"},
526+
tok=u1_token,
527+
)
528+
self.get_success(
529+
event_injection.inject_member_event(self.hs, room, "@u2:host2", "join")
530+
)
531+
self.get_success(
532+
event_injection.inject_member_event(self.hs, room, "@u3:other", "join")
533+
)
534+
535+
# create some events
536+
event_id_1 = self.helper.send(room, "hello", tok=u1_token)["event_id"]
537+
event_id_2 = self.helper.send(room, "world", tok=u1_token)["event_id"]
538+
# pretend that u3 changes their displayname
539+
event_id_3 = self.get_success(
540+
event_injection.inject_member_event(self.hs, room, "@u3:other", "join")
541+
).event_id
542+
543+
# destination_rooms should already be populated, but let us pretend that we already
544+
# sent (successfully) up to and including event id 1
545+
event_1 = self.get_success(self.hs.get_datastores().main.get_event(event_id_1))
546+
assert event_1.internal_metadata.stream_ordering is not None
547+
self.get_success(
548+
self.hs.get_datastores().main.set_destination_last_successful_stream_ordering(
549+
"host2", event_1.internal_metadata.stream_ordering
550+
)
551+
)
552+
553+
# also fetch event 2 so we can compare its stream ordering to the sender's
554+
# last_successful_stream_ordering later
555+
event_2 = self.get_success(self.hs.get_datastores().main.get_event(event_id_2))
556+
557+
# Mock event 3 as having partial state
558+
self.get_success(
559+
event_injection.mark_event_as_partial_state(self.hs, event_id_3, room)
560+
)
561+
562+
# Fail the test if we block on full state for event 3.
563+
async def mock_await_full_state(event_ids: Collection[str]) -> None:
564+
if event_id_3 in event_ids:
565+
raise AssertionError("Tried to await full state for event_id_3")
566+
567+
# ACT
568+
with mock.patch.object(
569+
self.hs.get_storage_controllers().state._partial_state_events_tracker,
570+
"await_full_state",
571+
mock_await_full_state,
572+
):
573+
self.get_success(per_dest_queue._catch_up_transmission_loop())
574+
575+
# ASSERT
576+
# We should have:
577+
# - not sent event 3: it's not ours, and the room is partial stated
578+
# - fallen back to sending event 2: it's the most recent event in the room
579+
# we tried to send to host2
580+
# - completed catch-up
581+
self.assertEqual(len(sent_pdus), 1)
582+
self.assertEqual(sent_pdus[0].event_id, event_id_2)
583+
self.assertFalse(per_dest_queue._catching_up)
584+
self.assertEqual(
585+
per_dest_queue._last_successful_stream_ordering,
586+
event_2.internal_metadata.stream_ordering,
587+
)

tests/test_utils/event_injection.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,34 @@ async def create_event(
102102
context = await unpersisted_context.persist(event)
103103

104104
return event, context
105+
106+
107+
async def mark_event_as_partial_state(
108+
hs: synapse.server.HomeServer,
109+
event_id: str,
110+
room_id: str,
111+
) -> None:
112+
"""
113+
(Falsely) mark an event as having partial state.
114+
115+
Naughty, but occasionally useful when checking that partial state doesn't
116+
block something from happening.
117+
118+
If the event already has partial state, this insert will fail (event_id is unique
119+
in this table).
120+
"""
121+
store = hs.get_datastores().main
122+
await store.db_pool.simple_upsert(
123+
table="partial_state_rooms",
124+
keyvalues={"room_id": room_id},
125+
values={},
126+
insertion_values={"room_id": room_id},
127+
)
128+
129+
await store.db_pool.simple_insert(
130+
table="partial_state_events",
131+
values={
132+
"room_id": room_id,
133+
"event_id": event_id,
134+
},
135+
)

0 commit comments

Comments
 (0)