Skip to content

Commit 34306be

Browse files
Only send rooms with updates down sliding sync (#17479)
Rather than always including all rooms in range. Also adds a pre-filter to rooms that checks the stream change cache to see if anything might have happened. Based on #17447 --------- Co-authored-by: Eric Eastwood <[email protected]>
1 parent be4a16f commit 34306be

File tree

5 files changed

+138
-30
lines changed

5 files changed

+138
-30
lines changed

changelog.d/17479.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Do not send down empty room entries down experimental sliding sync endpoint.

synapse/handlers/sliding_sync.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,51 @@ async def current_sync_for_user(
619619
# Fetch room data
620620
rooms: Dict[str, SlidingSyncResult.RoomResult] = {}
621621

622+
# Filter out rooms that haven't received updates and we've sent down
623+
# previously.
624+
if from_token:
625+
rooms_should_send = set()
626+
627+
# First we check if there are rooms that match a list/room
628+
# subscription and have updates we need to send (i.e. either because
629+
# we haven't sent the room down, or we have but there are missing
630+
# updates).
631+
for room_id in relevant_room_map:
632+
status = await self.connection_store.have_sent_room(
633+
sync_config,
634+
from_token.connection_position,
635+
room_id,
636+
)
637+
if (
638+
# The room was never sent down before so the client needs to know
639+
# about it regardless of any updates.
640+
status.status == HaveSentRoomFlag.NEVER
641+
# `PREVIOUSLY` literally means the "room was sent down before *AND*
642+
# there are updates we haven't sent down" so we already know this
643+
# room has updates.
644+
or status.status == HaveSentRoomFlag.PREVIOUSLY
645+
):
646+
rooms_should_send.add(room_id)
647+
elif status.status == HaveSentRoomFlag.LIVE:
648+
# We know that we've sent all updates up until `from_token`,
649+
# so we just need to check if there have been updates since
650+
# then.
651+
pass
652+
else:
653+
assert_never(status.status)
654+
655+
# We only need to check for new events since any state changes
656+
# will also come down as new events.
657+
rooms_that_have_updates = self.store.get_rooms_that_might_have_updates(
658+
relevant_room_map.keys(), from_token.stream_token.room_key
659+
)
660+
rooms_should_send.update(rooms_that_have_updates)
661+
relevant_room_map = {
662+
room_id: room_sync_config
663+
for room_id, room_sync_config in relevant_room_map.items()
664+
if room_id in rooms_should_send
665+
}
666+
622667
@trace
623668
@tag_args
624669
async def handle_room(room_id: str) -> None:
@@ -633,7 +678,9 @@ async def handle_room(room_id: str) -> None:
633678
to_token=to_token,
634679
)
635680

636-
rooms[room_id] = room_sync_result
681+
# Filter out empty room results during incremental sync
682+
if room_sync_result or not from_token:
683+
rooms[room_id] = room_sync_result
637684

638685
with start_active_span("sliding_sync.generate_room_entries"):
639686
await concurrently_execute(handle_room, relevant_room_map, 10)
@@ -2198,7 +2245,7 @@ class SlidingSyncConnectionStore:
21982245
a connection position of 5 might have totally different states on worker A and
21992246
worker B.
22002247
2201-
One complication that we need to deal with here is needing to handle requests being
2248+
One complication that we need to deal with here is needing to handle requests being
22022249
resent, i.e. if we sent down a room in a response that the client received, we must
22032250
consider the room *not* sent when we get the request again.
22042251

synapse/storage/databases/main/stream.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2104,3 +2104,13 @@ async def get_timeline_gaps(
21042104
return RoomStreamToken(stream=last_position.stream - 1)
21052105

21062106
return None
2107+
2108+
def get_rooms_that_might_have_updates(
2109+
self, room_ids: StrCollection, from_token: RoomStreamToken
2110+
) -> StrCollection:
2111+
"""Filters given room IDs down to those that might have updates, i.e.
2112+
removes rooms that definitely do not have updates.
2113+
"""
2114+
return self._events_stream_cache.get_entities_changed(
2115+
room_ids, from_token.stream
2116+
)

synapse/types/handlers/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,17 @@ class StrippedHero:
238238
notification_count: int
239239
highlight_count: int
240240

241+
def __bool__(self) -> bool:
242+
return (
243+
# If this is the first time the client is seeing the room, we should not filter it out
244+
# under any circumstance.
245+
self.initial
246+
# We need to let the client know if there are any new events
247+
or bool(self.required_state)
248+
or bool(self.timeline_events)
249+
or bool(self.stripped_state)
250+
)
251+
241252
@attr.s(slots=True, frozen=True, auto_attribs=True)
242253
class SlidingWindowList:
243254
"""
@@ -367,7 +378,11 @@ def __bool__(self) -> bool:
367378
to tell if the notifier needs to wait for more events when polling for
368379
events.
369380
"""
370-
return bool(self.lists or self.rooms or self.extensions)
381+
# We don't include `self.lists` here, as a) `lists` is always non-empty even if
382+
# there are no changes, and b) since we're sorting rooms by `stream_ordering` of
383+
# the latest activity, anything that would cause the order to change would end
384+
# up in `self.rooms` and cause us to send down the change.
385+
return bool(self.rooms or self.extensions)
371386

372387
@staticmethod
373388
def empty(next_pos: SlidingSyncStreamToken) -> "SlidingSyncResult":

tests/rest/client/test_sync.py

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969
)
7070
from tests.server import TimedOutException
7171
from tests.test_utils.event_injection import create_event, mark_event_as_partial_state
72-
from tests.unittest import skip_unless
7372

7473
logger = logging.getLogger(__name__)
7574

@@ -1656,12 +1655,6 @@ def test_wait_for_new_data(self) -> None:
16561655
channel.json_body["rooms"][room_id]["timeline"],
16571656
)
16581657

1659-
# TODO: Once we remove `ops`, we should be able to add a `RoomResult.__bool__` to
1660-
# check if there are any updates since the `from_token`.
1661-
@skip_unless(
1662-
False,
1663-
"Once we remove ops from the Sliding Sync response, this test should pass",
1664-
)
16651658
def test_wait_for_new_data_timeout(self) -> None:
16661659
"""
16671660
Test to make sure that the Sliding Sync request waits for new data to arrive but
@@ -1711,12 +1704,8 @@ def test_wait_for_new_data_timeout(self) -> None:
17111704
channel.await_result(timeout_ms=1200)
17121705
self.assertEqual(channel.code, 200, channel.json_body)
17131706

1714-
# We still see rooms because that's how Sliding Sync lists work but we reached
1715-
# the timeout before seeing them
1716-
self.assertEqual(
1717-
[event["event_id"] for event in channel.json_body["rooms"].keys()],
1718-
[room_id],
1719-
)
1707+
# There should be no room sent down.
1708+
self.assertFalse(channel.json_body["rooms"])
17201709

17211710
def test_filter_list(self) -> None:
17221711
"""
@@ -3556,19 +3545,7 @@ def test_rooms_ban_incremental_sync2(self) -> None:
35563545
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
35573546

35583547
# Nothing to see for this banned user in the room in the token range
3559-
self.assertIsNone(response_body["rooms"][room_id1].get("timeline"))
3560-
# No events returned in the timeline so nothing is "live"
3561-
self.assertEqual(
3562-
response_body["rooms"][room_id1]["num_live"],
3563-
0,
3564-
response_body["rooms"][room_id1],
3565-
)
3566-
# There aren't anymore events to paginate to in this range
3567-
self.assertEqual(
3568-
response_body["rooms"][room_id1]["limited"],
3569-
False,
3570-
response_body["rooms"][room_id1],
3571-
)
3548+
self.assertIsNone(response_body["rooms"].get(room_id1))
35723549

35733550
def test_rooms_no_required_state(self) -> None:
35743551
"""
@@ -3668,12 +3645,15 @@ def test_rooms_required_state_incremental_sync(self) -> None:
36683645
# This one doesn't exist in the room
36693646
[EventTypes.Tombstone, ""],
36703647
],
3671-
"timeline_limit": 0,
3648+
"timeline_limit": 1,
36723649
}
36733650
}
36743651
}
36753652
_, from_token = self.do_sync(sync_body, tok=user1_tok)
36763653

3654+
# Send a message so the room comes down sync.
3655+
self.helper.send(room_id1, "msg", tok=user1_tok)
3656+
36773657
# Make the incremental Sliding Sync request
36783658
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
36793659

@@ -4880,6 +4860,61 @@ def test_rooms_timeline_incremental_sync_NEVER(self) -> None:
48804860
self.assertEqual(response_body["rooms"][room_id1]["limited"], True)
48814861
self.assertEqual(response_body["rooms"][room_id1]["initial"], True)
48824862

4863+
def test_rooms_with_no_updates_do_not_come_down_incremental_sync(self) -> None:
4864+
"""
4865+
Test that rooms with no updates are returned in subsequent incremental
4866+
syncs.
4867+
"""
4868+
4869+
user1_id = self.register_user("user1", "pass")
4870+
user1_tok = self.login(user1_id, "pass")
4871+
4872+
room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
4873+
4874+
sync_body = {
4875+
"lists": {
4876+
"foo-list": {
4877+
"ranges": [[0, 1]],
4878+
"required_state": [],
4879+
"timeline_limit": 0,
4880+
}
4881+
}
4882+
}
4883+
4884+
_, from_token = self.do_sync(sync_body, tok=user1_tok)
4885+
4886+
# Make the incremental Sliding Sync request
4887+
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
4888+
4889+
# Nothing has happened in the room, so the room should not come down
4890+
# /sync.
4891+
self.assertIsNone(response_body["rooms"].get(room_id1))
4892+
4893+
def test_empty_initial_room_comes_down_sync(self) -> None:
4894+
"""
4895+
Test that rooms come down /sync even with empty required state and
4896+
timeline limit in initial sync.
4897+
"""
4898+
4899+
user1_id = self.register_user("user1", "pass")
4900+
user1_tok = self.login(user1_id, "pass")
4901+
4902+
room_id1 = self.helper.create_room_as(user1_id, tok=user1_tok)
4903+
4904+
sync_body = {
4905+
"lists": {
4906+
"foo-list": {
4907+
"ranges": [[0, 1]],
4908+
"required_state": [],
4909+
"timeline_limit": 0,
4910+
}
4911+
}
4912+
}
4913+
4914+
# Make the Sliding Sync request
4915+
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
4916+
self.assertEqual(response_body["rooms"][room_id1]["initial"], True)
4917+
48834918

48844919
class SlidingSyncToDeviceExtensionTestCase(SlidingSyncBase):
48854920
"""Tests for the to-device sliding sync extension"""

0 commit comments

Comments
 (0)