Skip to content

Commit a308d99

Browse files
Sliding Sync: Exclude partially stated rooms if we must await full state (#17538)
Previously, we just had very basic partial room exclusion based on whether we were lazy-loading room members. Now with this PR, we added `must_await_full_state(...)` with rules to check if we have a we're only requesting `required_state` which is completely satisfied even with partial state. Partially-stated rooms should have all state events except for remote membership events so if we require a remote membership event anywhere, then we need to return `True`.
1 parent a9fc1fd commit a308d99

File tree

3 files changed

+255
-45
lines changed

3 files changed

+255
-45
lines changed

changelog.d/17538.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Better exclude partially stated rooms if we must await full state in experimental [MSC3575](https://github.com/matrix-org/matrix-spec-proposals/pull/3575) Sliding Sync `/sync` endpoint.

synapse/handlers/sliding_sync.py

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from typing import (
2525
TYPE_CHECKING,
2626
Any,
27+
Callable,
2728
Dict,
2829
Final,
2930
List,
@@ -366,6 +367,73 @@ def combine_room_sync_config(
366367
else:
367368
self.required_state_map[state_type].add(state_key)
368369

370+
def must_await_full_state(
371+
self,
372+
is_mine_id: Callable[[str], bool],
373+
) -> bool:
374+
"""
375+
Check if we have a we're only requesting `required_state` which is completely
376+
satisfied even with partial state, then we don't need to `await_full_state` before
377+
we can return it.
378+
379+
Also see `StateFilter.must_await_full_state(...)` for comparison
380+
381+
Partially-stated rooms should have all state events except for remote membership
382+
events so if we require a remote membership event anywhere, then we need to
383+
return `True` (requires full state).
384+
385+
Args:
386+
is_mine_id: a callable which confirms if a given state_key matches a mxid
387+
of a local user
388+
"""
389+
wildcard_state_keys = self.required_state_map.get(StateValues.WILDCARD)
390+
# Requesting *all* state in the room so we have to wait
391+
if (
392+
wildcard_state_keys is not None
393+
and StateValues.WILDCARD in wildcard_state_keys
394+
):
395+
return True
396+
397+
# If the wildcards don't refer to remote user IDs, then we don't need to wait
398+
# for full state.
399+
if wildcard_state_keys is not None:
400+
for possible_user_id in wildcard_state_keys:
401+
if not possible_user_id[0].startswith(UserID.SIGIL):
402+
# Not a user ID
403+
continue
404+
405+
localpart_hostname = possible_user_id.split(":", 1)
406+
if len(localpart_hostname) < 2:
407+
# Not a user ID
408+
continue
409+
410+
if not is_mine_id(possible_user_id):
411+
return True
412+
413+
membership_state_keys = self.required_state_map.get(EventTypes.Member)
414+
# We aren't requesting any membership events at all so the partial state will
415+
# cover us.
416+
if membership_state_keys is None:
417+
return False
418+
419+
# If we're requesting entirely local users, the partial state will cover us.
420+
for user_id in membership_state_keys:
421+
if user_id == StateValues.ME:
422+
continue
423+
# We're lazy-loading membership so we can just return the state we have.
424+
# Lazy-loading means we include membership for any event `sender` in the
425+
# timeline but since we had to auth those timeline events, we will have the
426+
# membership state for them (including from remote senders).
427+
elif user_id == StateValues.LAZY:
428+
continue
429+
elif user_id == StateValues.WILDCARD:
430+
return False
431+
elif not is_mine_id(user_id):
432+
return True
433+
434+
# Local users only so the partial state will cover us.
435+
return False
436+
369437

370438
class StateValues:
371439
"""
@@ -395,6 +463,7 @@ def __init__(self, hs: "HomeServer"):
395463
self.device_handler = hs.get_device_handler()
396464
self.push_rules_handler = hs.get_push_rules_handler()
397465
self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync
466+
self.is_mine_id = hs.is_mine_id
398467

399468
self.connection_store = SlidingSyncConnectionStore()
400469

@@ -575,19 +644,10 @@ async def current_sync_for_user(
575644
# Since creating the `RoomSyncConfig` takes some work, let's just do it
576645
# once and make a copy whenever we need it.
577646
room_sync_config = RoomSyncConfig.from_room_config(list_config)
578-
membership_state_keys = room_sync_config.required_state_map.get(
579-
EventTypes.Member
580-
)
581-
# Also see `StateFilter.must_await_full_state(...)` for comparison
582-
lazy_loading = (
583-
membership_state_keys is not None
584-
and StateValues.LAZY in membership_state_keys
585-
)
586647

587-
if not lazy_loading:
588-
# Exclude partially-stated rooms unless the `required_state`
589-
# only has `["m.room.member", "$LAZY"]` for membership
590-
# (lazy-loading room members).
648+
# Exclude partially-stated rooms if we must wait for the room to be
649+
# fully-stated
650+
if room_sync_config.must_await_full_state(self.is_mine_id):
591651
filtered_sync_room_map = {
592652
room_id: room
593653
for room_id, room in filtered_sync_room_map.items()
@@ -654,6 +714,12 @@ async def current_sync_for_user(
654714
# Handle room subscriptions
655715
if has_room_subscriptions and sync_config.room_subscriptions is not None:
656716
with start_active_span("assemble_room_subscriptions"):
717+
# Find which rooms are partially stated and may need to be filtered out
718+
# depending on the `required_state` requested (see below).
719+
partial_state_room_map = await self.store.is_partial_state_room_batched(
720+
sync_config.room_subscriptions.keys()
721+
)
722+
657723
for (
658724
room_id,
659725
room_subscription,
@@ -677,12 +743,20 @@ async def current_sync_for_user(
677743
)
678744

679745
# Take the superset of the `RoomSyncConfig` for each room.
680-
#
681-
# Update our `relevant_room_map` with the room we're going to display
682-
# and need to fetch more info about.
683746
room_sync_config = RoomSyncConfig.from_room_config(
684747
room_subscription
685748
)
749+
750+
# Exclude partially-stated rooms if we must wait for the room to be
751+
# fully-stated
752+
if room_sync_config.must_await_full_state(self.is_mine_id):
753+
if partial_state_room_map.get(room_id):
754+
continue
755+
756+
all_rooms.add(room_id)
757+
758+
# Update our `relevant_room_map` with the room we're going to display
759+
# and need to fetch more info about.
686760
existing_room_sync_config = relevant_room_map.get(room_id)
687761
if existing_room_sync_config is not None:
688762
existing_room_sync_config.combine_room_sync_config(

tests/rest/client/sliding_sync/test_rooms_required_state.py

Lines changed: 165 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -631,8 +631,7 @@ def test_rooms_required_state_combine_superset(self) -> None:
631631

632632
def test_rooms_required_state_partial_state(self) -> None:
633633
"""
634-
Test partially-stated room are excluded unless `rooms.required_state` is
635-
lazy-loading room members.
634+
Test partially-stated room are excluded if they require full state.
636635
"""
637636
user1_id = self.register_user("user1", "pass")
638637
user1_tok = self.login(user1_id, "pass")
@@ -649,59 +648,195 @@ def test_rooms_required_state_partial_state(self) -> None:
649648
mark_event_as_partial_state(self.hs, join_response2["event_id"], room_id2)
650649
)
651650

652-
# Make the Sliding Sync request (NOT lazy-loading room members)
651+
# Make the Sliding Sync request with examples where `must_await_full_state()` is
652+
# `False`
653653
sync_body = {
654654
"lists": {
655-
"foo-list": {
655+
"no-state-list": {
656+
"ranges": [[0, 1]],
657+
"required_state": [],
658+
"timeline_limit": 0,
659+
},
660+
"other-state-list": {
656661
"ranges": [[0, 1]],
657662
"required_state": [
658663
[EventTypes.Create, ""],
659664
],
660665
"timeline_limit": 0,
661666
},
667+
"lazy-load-list": {
668+
"ranges": [[0, 1]],
669+
"required_state": [
670+
[EventTypes.Create, ""],
671+
# Lazy-load room members
672+
[EventTypes.Member, StateValues.LAZY],
673+
# Local member
674+
[EventTypes.Member, user2_id],
675+
],
676+
"timeline_limit": 0,
677+
},
678+
"local-members-only-list": {
679+
"ranges": [[0, 1]],
680+
"required_state": [
681+
# Own user ID
682+
[EventTypes.Member, user1_id],
683+
# Local member
684+
[EventTypes.Member, user2_id],
685+
],
686+
"timeline_limit": 0,
687+
},
688+
"me-list": {
689+
"ranges": [[0, 1]],
690+
"required_state": [
691+
# Own user ID
692+
[EventTypes.Member, StateValues.ME],
693+
# Local member
694+
[EventTypes.Member, user2_id],
695+
],
696+
"timeline_limit": 0,
697+
},
698+
"wildcard-type-local-state-key-list": {
699+
"ranges": [[0, 1]],
700+
"required_state": [
701+
["*", user1_id],
702+
# Not a user ID
703+
["*", "foobarbaz"],
704+
# Not a user ID
705+
["*", "foo.bar.baz"],
706+
# Not a user ID
707+
["*", "@foo"],
708+
],
709+
"timeline_limit": 0,
710+
},
662711
}
663712
}
664713
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
665714

666-
# Make sure the list includes room1 but room2 is excluded because it's still
667-
# partially-stated
668-
self.assertListEqual(
669-
list(response_body["lists"]["foo-list"]["ops"]),
670-
[
671-
{
672-
"op": "SYNC",
673-
"range": [0, 1],
674-
"room_ids": [room_id1],
715+
# The list should include both rooms now because we don't need full state
716+
for list_key in response_body["lists"].keys():
717+
self.assertIncludes(
718+
set(response_body["lists"][list_key]["ops"][0]["room_ids"]),
719+
{room_id2, room_id1},
720+
exact=True,
721+
message=f"Expected all rooms to show up for list_key={list_key}. Response "
722+
+ str(response_body["lists"][list_key]),
723+
)
724+
725+
# Take each of the list variants and apply them to room subscriptions to make
726+
# sure the same rules apply
727+
for list_key in sync_body["lists"].keys():
728+
sync_body_for_subscriptions = {
729+
"room_subscriptions": {
730+
room_id1: {
731+
"required_state": sync_body["lists"][list_key][
732+
"required_state"
733+
],
734+
"timeline_limit": 0,
735+
},
736+
room_id2: {
737+
"required_state": sync_body["lists"][list_key][
738+
"required_state"
739+
],
740+
"timeline_limit": 0,
741+
},
675742
}
676-
],
677-
response_body["lists"]["foo-list"],
678-
)
743+
}
744+
response_body, _ = self.do_sync(sync_body_for_subscriptions, tok=user1_tok)
745+
746+
self.assertIncludes(
747+
set(response_body["rooms"].keys()),
748+
{room_id2, room_id1},
749+
exact=True,
750+
message=f"Expected all rooms to show up for test_key={list_key}.",
751+
)
679752

680-
# Make the Sliding Sync request (with lazy-loading room members)
753+
# =====================================================================
754+
755+
# Make the Sliding Sync request with examples where `must_await_full_state()` is
756+
# `True`
681757
sync_body = {
682758
"lists": {
683-
"foo-list": {
759+
"wildcard-list": {
760+
"ranges": [[0, 1]],
761+
"required_state": [
762+
["*", "*"],
763+
],
764+
"timeline_limit": 0,
765+
},
766+
"wildcard-type-remote-state-key-list": {
767+
"ranges": [[0, 1]],
768+
"required_state": [
769+
["*", "@some:remote"],
770+
# Not a user ID
771+
["*", "foobarbaz"],
772+
# Not a user ID
773+
["*", "foo.bar.baz"],
774+
# Not a user ID
775+
["*", "@foo"],
776+
],
777+
"timeline_limit": 0,
778+
},
779+
"remote-member-list": {
780+
"ranges": [[0, 1]],
781+
"required_state": [
782+
# Own user ID
783+
[EventTypes.Member, user1_id],
784+
# Remote member
785+
[EventTypes.Member, "@some:remote"],
786+
# Local member
787+
[EventTypes.Member, user2_id],
788+
],
789+
"timeline_limit": 0,
790+
},
791+
"lazy-but-remote-member-list": {
684792
"ranges": [[0, 1]],
685793
"required_state": [
686-
[EventTypes.Create, ""],
687794
# Lazy-load room members
688795
[EventTypes.Member, StateValues.LAZY],
796+
# Remote member
797+
[EventTypes.Member, "@some:remote"],
689798
],
690799
"timeline_limit": 0,
691800
},
692801
}
693802
}
694803
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
695804

696-
# The list should include both rooms now because we're lazy-loading room members
697-
self.assertListEqual(
698-
list(response_body["lists"]["foo-list"]["ops"]),
699-
[
700-
{
701-
"op": "SYNC",
702-
"range": [0, 1],
703-
"room_ids": [room_id2, room_id1],
805+
# Make sure the list includes room1 but room2 is excluded because it's still
806+
# partially-stated
807+
for list_key in response_body["lists"].keys():
808+
self.assertIncludes(
809+
set(response_body["lists"][list_key]["ops"][0]["room_ids"]),
810+
{room_id1},
811+
exact=True,
812+
message=f"Expected only fully-stated rooms to show up for list_key={list_key}. Response "
813+
+ str(response_body["lists"][list_key]),
814+
)
815+
816+
# Take each of the list variants and apply them to room subscriptions to make
817+
# sure the same rules apply
818+
for list_key in sync_body["lists"].keys():
819+
sync_body_for_subscriptions = {
820+
"room_subscriptions": {
821+
room_id1: {
822+
"required_state": sync_body["lists"][list_key][
823+
"required_state"
824+
],
825+
"timeline_limit": 0,
826+
},
827+
room_id2: {
828+
"required_state": sync_body["lists"][list_key][
829+
"required_state"
830+
],
831+
"timeline_limit": 0,
832+
},
704833
}
705-
],
706-
response_body["lists"]["foo-list"],
707-
)
834+
}
835+
response_body, _ = self.do_sync(sync_body_for_subscriptions, tok=user1_tok)
836+
837+
self.assertIncludes(
838+
set(response_body["rooms"].keys()),
839+
{room_id1},
840+
exact=True,
841+
message=f"Expected only fully-stated rooms to show up for test_key={list_key}.",
842+
)

0 commit comments

Comments
 (0)