0
0
Fork 1
mirror of https://mau.dev/maunium/synapse.git synced 2024-11-14 14:01:59 +01:00

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`.
This commit is contained in:
Eric Eastwood 2024-08-13 12:27:42 -05:00 committed by GitHub
parent a9fc1fd112
commit a308d99f30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 266 additions and 56 deletions

1
changelog.d/17538.bugfix Normal file
View file

@ -0,0 +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.

View file

@ -24,6 +24,7 @@ from itertools import chain
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Callable,
Dict, Dict,
Final, Final,
List, List,
@ -366,6 +367,73 @@ class RoomSyncConfig:
else: else:
self.required_state_map[state_type].add(state_key) self.required_state_map[state_type].add(state_key)
def must_await_full_state(
self,
is_mine_id: Callable[[str], bool],
) -> bool:
"""
Check if we have a we're only requesting `required_state` which is completely
satisfied even with partial state, then we don't need to `await_full_state` before
we can return it.
Also see `StateFilter.must_await_full_state(...)` for comparison
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` (requires full state).
Args:
is_mine_id: a callable which confirms if a given state_key matches a mxid
of a local user
"""
wildcard_state_keys = self.required_state_map.get(StateValues.WILDCARD)
# Requesting *all* state in the room so we have to wait
if (
wildcard_state_keys is not None
and StateValues.WILDCARD in wildcard_state_keys
):
return True
# If the wildcards don't refer to remote user IDs, then we don't need to wait
# for full state.
if wildcard_state_keys is not None:
for possible_user_id in wildcard_state_keys:
if not possible_user_id[0].startswith(UserID.SIGIL):
# Not a user ID
continue
localpart_hostname = possible_user_id.split(":", 1)
if len(localpart_hostname) < 2:
# Not a user ID
continue
if not is_mine_id(possible_user_id):
return True
membership_state_keys = self.required_state_map.get(EventTypes.Member)
# We aren't requesting any membership events at all so the partial state will
# cover us.
if membership_state_keys is None:
return False
# If we're requesting entirely local users, the partial state will cover us.
for user_id in membership_state_keys:
if user_id == StateValues.ME:
continue
# We're lazy-loading membership so we can just return the state we have.
# Lazy-loading means we include membership for any event `sender` in the
# timeline but since we had to auth those timeline events, we will have the
# membership state for them (including from remote senders).
elif user_id == StateValues.LAZY:
continue
elif user_id == StateValues.WILDCARD:
return False
elif not is_mine_id(user_id):
return True
# Local users only so the partial state will cover us.
return False
class StateValues: class StateValues:
""" """
@ -395,6 +463,7 @@ class SlidingSyncHandler:
self.device_handler = hs.get_device_handler() self.device_handler = hs.get_device_handler()
self.push_rules_handler = hs.get_push_rules_handler() self.push_rules_handler = hs.get_push_rules_handler()
self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync self.rooms_to_exclude_globally = hs.config.server.rooms_to_exclude_from_sync
self.is_mine_id = hs.is_mine_id
self.connection_store = SlidingSyncConnectionStore() self.connection_store = SlidingSyncConnectionStore()
@ -575,19 +644,10 @@ class SlidingSyncHandler:
# Since creating the `RoomSyncConfig` takes some work, let's just do it # Since creating the `RoomSyncConfig` takes some work, let's just do it
# once and make a copy whenever we need it. # once and make a copy whenever we need it.
room_sync_config = RoomSyncConfig.from_room_config(list_config) room_sync_config = RoomSyncConfig.from_room_config(list_config)
membership_state_keys = room_sync_config.required_state_map.get(
EventTypes.Member
)
# Also see `StateFilter.must_await_full_state(...)` for comparison
lazy_loading = (
membership_state_keys is not None
and StateValues.LAZY in membership_state_keys
)
if not lazy_loading: # Exclude partially-stated rooms if we must wait for the room to be
# Exclude partially-stated rooms unless the `required_state` # fully-stated
# only has `["m.room.member", "$LAZY"]` for membership if room_sync_config.must_await_full_state(self.is_mine_id):
# (lazy-loading room members).
filtered_sync_room_map = { filtered_sync_room_map = {
room_id: room room_id: room
for room_id, room in filtered_sync_room_map.items() for room_id, room in filtered_sync_room_map.items()
@ -654,6 +714,12 @@ class SlidingSyncHandler:
# Handle room subscriptions # Handle room subscriptions
if has_room_subscriptions and sync_config.room_subscriptions is not None: if has_room_subscriptions and sync_config.room_subscriptions is not None:
with start_active_span("assemble_room_subscriptions"): with start_active_span("assemble_room_subscriptions"):
# Find which rooms are partially stated and may need to be filtered out
# depending on the `required_state` requested (see below).
partial_state_room_map = await self.store.is_partial_state_room_batched(
sync_config.room_subscriptions.keys()
)
for ( for (
room_id, room_id,
room_subscription, room_subscription,
@ -677,12 +743,20 @@ class SlidingSyncHandler:
) )
# Take the superset of the `RoomSyncConfig` for each room. # Take the superset of the `RoomSyncConfig` for each room.
#
# Update our `relevant_room_map` with the room we're going to display
# and need to fetch more info about.
room_sync_config = RoomSyncConfig.from_room_config( room_sync_config = RoomSyncConfig.from_room_config(
room_subscription room_subscription
) )
# Exclude partially-stated rooms if we must wait for the room to be
# fully-stated
if room_sync_config.must_await_full_state(self.is_mine_id):
if partial_state_room_map.get(room_id):
continue
all_rooms.add(room_id)
# Update our `relevant_room_map` with the room we're going to display
# and need to fetch more info about.
existing_room_sync_config = relevant_room_map.get(room_id) existing_room_sync_config = relevant_room_map.get(room_id)
if existing_room_sync_config is not None: if existing_room_sync_config is not None:
existing_room_sync_config.combine_room_sync_config( existing_room_sync_config.combine_room_sync_config(

View file

@ -631,8 +631,7 @@ class SlidingSyncRoomsRequiredStateTestCase(SlidingSyncBase):
def test_rooms_required_state_partial_state(self) -> None: def test_rooms_required_state_partial_state(self) -> None:
""" """
Test partially-stated room are excluded unless `rooms.required_state` is Test partially-stated room are excluded if they require full state.
lazy-loading room members.
""" """
user1_id = self.register_user("user1", "pass") user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass") user1_tok = self.login(user1_id, "pass")
@ -649,59 +648,195 @@ class SlidingSyncRoomsRequiredStateTestCase(SlidingSyncBase):
mark_event_as_partial_state(self.hs, join_response2["event_id"], room_id2) mark_event_as_partial_state(self.hs, join_response2["event_id"], room_id2)
) )
# Make the Sliding Sync request (NOT lazy-loading room members) # Make the Sliding Sync request with examples where `must_await_full_state()` is
# `False`
sync_body = { sync_body = {
"lists": { "lists": {
"foo-list": { "no-state-list": {
"ranges": [[0, 1]],
"required_state": [],
"timeline_limit": 0,
},
"other-state-list": {
"ranges": [[0, 1]], "ranges": [[0, 1]],
"required_state": [ "required_state": [
[EventTypes.Create, ""], [EventTypes.Create, ""],
], ],
"timeline_limit": 0, "timeline_limit": 0,
}, },
"lazy-load-list": {
"ranges": [[0, 1]],
"required_state": [
[EventTypes.Create, ""],
# Lazy-load room members
[EventTypes.Member, StateValues.LAZY],
# Local member
[EventTypes.Member, user2_id],
],
"timeline_limit": 0,
},
"local-members-only-list": {
"ranges": [[0, 1]],
"required_state": [
# Own user ID
[EventTypes.Member, user1_id],
# Local member
[EventTypes.Member, user2_id],
],
"timeline_limit": 0,
},
"me-list": {
"ranges": [[0, 1]],
"required_state": [
# Own user ID
[EventTypes.Member, StateValues.ME],
# Local member
[EventTypes.Member, user2_id],
],
"timeline_limit": 0,
},
"wildcard-type-local-state-key-list": {
"ranges": [[0, 1]],
"required_state": [
["*", user1_id],
# Not a user ID
["*", "foobarbaz"],
# Not a user ID
["*", "foo.bar.baz"],
# Not a user ID
["*", "@foo"],
],
"timeline_limit": 0,
},
}
}
response_body, _ = self.do_sync(sync_body, tok=user1_tok)
# The list should include both rooms now because we don't need full state
for list_key in response_body["lists"].keys():
self.assertIncludes(
set(response_body["lists"][list_key]["ops"][0]["room_ids"]),
{room_id2, room_id1},
exact=True,
message=f"Expected all rooms to show up for list_key={list_key}. Response "
+ str(response_body["lists"][list_key]),
)
# Take each of the list variants and apply them to room subscriptions to make
# sure the same rules apply
for list_key in sync_body["lists"].keys():
sync_body_for_subscriptions = {
"room_subscriptions": {
room_id1: {
"required_state": sync_body["lists"][list_key][
"required_state"
],
"timeline_limit": 0,
},
room_id2: {
"required_state": sync_body["lists"][list_key][
"required_state"
],
"timeline_limit": 0,
},
}
}
response_body, _ = self.do_sync(sync_body_for_subscriptions, tok=user1_tok)
self.assertIncludes(
set(response_body["rooms"].keys()),
{room_id2, room_id1},
exact=True,
message=f"Expected all rooms to show up for test_key={list_key}.",
)
# =====================================================================
# Make the Sliding Sync request with examples where `must_await_full_state()` is
# `True`
sync_body = {
"lists": {
"wildcard-list": {
"ranges": [[0, 1]],
"required_state": [
["*", "*"],
],
"timeline_limit": 0,
},
"wildcard-type-remote-state-key-list": {
"ranges": [[0, 1]],
"required_state": [
["*", "@some:remote"],
# Not a user ID
["*", "foobarbaz"],
# Not a user ID
["*", "foo.bar.baz"],
# Not a user ID
["*", "@foo"],
],
"timeline_limit": 0,
},
"remote-member-list": {
"ranges": [[0, 1]],
"required_state": [
# Own user ID
[EventTypes.Member, user1_id],
# Remote member
[EventTypes.Member, "@some:remote"],
# Local member
[EventTypes.Member, user2_id],
],
"timeline_limit": 0,
},
"lazy-but-remote-member-list": {
"ranges": [[0, 1]],
"required_state": [
# Lazy-load room members
[EventTypes.Member, StateValues.LAZY],
# Remote member
[EventTypes.Member, "@some:remote"],
],
"timeline_limit": 0,
},
} }
} }
response_body, _ = self.do_sync(sync_body, tok=user1_tok) response_body, _ = self.do_sync(sync_body, tok=user1_tok)
# Make sure the list includes room1 but room2 is excluded because it's still # Make sure the list includes room1 but room2 is excluded because it's still
# partially-stated # partially-stated
self.assertListEqual( for list_key in response_body["lists"].keys():
list(response_body["lists"]["foo-list"]["ops"]), self.assertIncludes(
[ set(response_body["lists"][list_key]["ops"][0]["room_ids"]),
{ {room_id1},
"op": "SYNC", exact=True,
"range": [0, 1], message=f"Expected only fully-stated rooms to show up for list_key={list_key}. Response "
"room_ids": [room_id1], + str(response_body["lists"][list_key]),
}
],
response_body["lists"]["foo-list"],
) )
# Make the Sliding Sync request (with lazy-loading room members) # Take each of the list variants and apply them to room subscriptions to make
sync_body = { # sure the same rules apply
"lists": { for list_key in sync_body["lists"].keys():
"foo-list": { sync_body_for_subscriptions = {
"ranges": [[0, 1]], "room_subscriptions": {
"required_state": [ room_id1: {
[EventTypes.Create, ""], "required_state": sync_body["lists"][list_key][
# Lazy-load room members "required_state"
[EventTypes.Member, StateValues.LAZY], ],
"timeline_limit": 0,
},
room_id2: {
"required_state": sync_body["lists"][list_key][
"required_state"
], ],
"timeline_limit": 0, "timeline_limit": 0,
}, },
} }
} }
response_body, _ = self.do_sync(sync_body, tok=user1_tok) response_body, _ = self.do_sync(sync_body_for_subscriptions, tok=user1_tok)
# The list should include both rooms now because we're lazy-loading room members self.assertIncludes(
self.assertListEqual( set(response_body["rooms"].keys()),
list(response_body["lists"]["foo-list"]["ops"]), {room_id1},
[ exact=True,
{ message=f"Expected only fully-stated rooms to show up for test_key={list_key}.",
"op": "SYNC",
"range": [0, 1],
"room_ids": [room_id2, room_id1],
}
],
response_body["lists"]["foo-list"],
) )