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

Sliding Sync: Lazy-loading room members on incremental sync (remember memberships) #17809

Open
wants to merge 31 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
af9c72d
Correctly changes to required state config
erikjohnston Oct 1, 2024
2fd3a5a
Add tests
erikjohnston Oct 3, 2024
5e66e98
Newsfile
erikjohnston Oct 3, 2024
596581a
Sliding Sync: Parameterize and add more tests for tracking changes to…
MadLittleMods Oct 9, 2024
4b631a5
Apply suggestions from code review
erikjohnston Oct 9, 2024
e913037
Fixup
erikjohnston Oct 9, 2024
35532d6
comment __bool__
erikjohnston Oct 9, 2024
859d4a8
Fix handling of your own user ID
erikjohnston Oct 9, 2024
56565dc
Remove unused var
erikjohnston Oct 9, 2024
0d0207e
Remove TODOs
erikjohnston Oct 9, 2024
fafb2fe
Move comment down
erikjohnston Oct 9, 2024
c23d326
Clarify None comment
erikjohnston Oct 9, 2024
6391b0f
LAZY only applies to memberships
erikjohnston Oct 9, 2024
fbe9894
Lazy-loading room members on incremental sync (remember memberships)
MadLittleMods Oct 9, 2024
b9a8780
Add changelog
MadLittleMods Oct 9, 2024
760810f
Re-use `invalidated_state_keys`
MadLittleMods Oct 9, 2024
ddc9769
Better clarify `$ME` translation
MadLittleMods Oct 9, 2024
8b906c4
Fill in TODO
MadLittleMods Oct 9, 2024
2080cc5
Fix grammar
MadLittleMods Oct 9, 2024
0087ad2
Move `prev_room_sync_config` up
MadLittleMods Oct 9, 2024
f8feef5
Move `prev_room_sync_config` up
MadLittleMods Oct 9, 2024
a566347
Merge branch 'develop' into erikj/ss_required_state
erikjohnston Oct 10, 2024
4e662da
Comment why we're making new sets
MadLittleMods Oct 10, 2024
fb5bca4
Merge branch 'erikj/ss_required_state' into madlittlemods/sliding-syn…
MadLittleMods Oct 10, 2024
7e667fe
Merge branch 'develop' into madlittlemods/sliding-sync-lazy-load-memb…
MadLittleMods Oct 14, 2024
aec7455
Limit the number of state_keys that we remember
MadLittleMods Oct 16, 2024
87497c8
Update comments
MadLittleMods Oct 16, 2024
525222b
Fix bug when requesting more than we can remember
MadLittleMods Oct 16, 2024
90f0741
Merge branch 'develop' into madlittlemods/sliding-sync-lazy-load-memb…
MadLittleMods Oct 16, 2024
96c9a91
Fix bug remembering too much when over limit
MadLittleMods Oct 16, 2024
cf13ca0
Merge branch 'develop' into madlittlemods/sliding-sync-lazy-load-memb…
MadLittleMods Oct 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/17809.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix bug with sliding sync where `$LAZY`-loading room members would not return `required_state` membership in incremental syncs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am slightly worried about the increase in the size of the tables if we keep adding more and more members in there, but I'll have a little think about how we might make that more efficient.

More concretely: there's a few concerns I have:

  1. Over the lifetime of a connection the size of the store required state could grow very large (think Matrix HQ).
  2. Currently, we pull out everything when we get a request, so the size of the data matters
  3. Every time we change the state we end up copying all the rows from the previous connection position to the new position, so the amount of data again matters.

On point 3. I think we can change the DB tables up a bit so that we have one table that holds the base values that apply to all live positions in the connection (there will be max two), and then another table that are the delta from the base to that position. This means that we'd only need to copy over the deltas to the base and insert new deltas whenever we persist a new position (which should be a lot less data).

I don't really know what we do about not pulling lots of data from the DB. We could only pull out that data for the rooms we're sending down? Or maybe we we don't pull it out and instead query the DB for "which of these users have we previously sent membership down for?", though that feels like a bigger change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could only pull out that data for the rooms we're sending down?

This would probably be good to do in any case.

But it seems we're also concerned with how big an individual (room_id, user_id) could get.

I don't really know what we do about not pulling lots of data from the DB. We could only pull out that data for the rooms we're sending down? Or maybe we we don't pull it out and instead query the DB for "which of these users have we previously sent membership down for?", though that feels like a bigger change.

Are we set on wanting to track it? The dumb simple solution is to not track it at all which means returning a membership event for any timeline events.

Another alternative is that we could throw away a type when the state_key's grow too large. Or just throw the whole required_state_map when it grows too large. When I say throw-away, I just mean reset to whatever the requested required_state_map has. This is a decent middle ground between tracking nothing and everything. Ideally, we could kick out entries by recency but the complexity is probably not needed. It also doesn't require us to change our database schema.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we do want to track the sent memberships, otherwise you are often just doubling the number of events you have to pull out and send to the clients.

Another alternative is that we could throw away a type when the state_key's grow too large. Or just throw the whole required_state_map when it grows too large. When I say throw-away, I just mean reset to whatever the requested required_state_map has. This is a decent middle ground between tracking nothing and everything. Ideally, we could kick out entries by recency but the complexity is probably not needed. It also doesn't require us to change our database schema.

This is probably a decent idea that should be fairly easy to do? Even something like "if we have $LAZY and the set of members we've sent down is bigger than 100, reset", or something. It's not pretty but a) means for most rooms we'll never reset, and b) most of the time we won't send down redundant memberships.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to limit the state_keys we remember from previous requests to 100 for any type (also added tests).

MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
128 changes: 93 additions & 35 deletions synapse/handlers/sliding_sync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,14 @@ async def get_room_sync_data(
#
# Calculate the `StateFilter` based on the `required_state` for the room
required_state_filter = StateFilter.none()
# The requested `required_state_map` with the lazy membership expanded and
# `$ME` replaced with the user's ID. This allows us to see what membership we've
# sent down to the client in the next request.
#
# Make a copy so we can modify it. Still need to be careful to make a copy of
# the state key sets if we want to add/remove from them. We could make a deep
# copy but this saves us some work.
expanded_required_state_map = dict(room_sync_config.required_state_map)
if room_membership_for_user_at_to_token.membership not in (
Membership.INVITE,
Membership.KNOCK,
Expand Down Expand Up @@ -938,21 +946,48 @@ async def get_room_sync_data(
):
lazy_load_room_members = True
# Everyone in the timeline is relevant
#
# FIXME: We probably also care about invite, ban, kick, targets, etc
# but the spec only mentions "senders".
timeline_membership: Set[str] = set()
if timeline_events is not None:
for timeline_event in timeline_events:
timeline_membership.add(timeline_event.sender)

# Update the required state filter so we pick up the new
# membership
for user_id in timeline_membership:
required_state_types.append(
(EventTypes.Member, user_id)
)

# FIXME: We probably also care about invite, ban, kick, targets, etc
# but the spec only mentions "senders".
# Add an explicit entry for each user in the timeline
#
# Make a new set or copy of the state key set so we can
# modify it without affecting the original
# `required_state_map`
expanded_required_state_map[EventTypes.Member] = (
expanded_required_state_map.get(
EventTypes.Member, set()
)
| timeline_membership
)
elif state_key == StateValues.ME:
num_others += 1
required_state_types.append((state_type, user.to_string()))
# Replace `$ME` with the user's ID so we can deduplicate
# when someone requests the same state with `$ME` or with
# their user ID.
#
# Make a new set or copy of the state key set so we can
# modify it without affecting the original
# `required_state_map`
expanded_required_state_map[EventTypes.Member] = (
expanded_required_state_map.get(
EventTypes.Member, set()
)
| {user.to_string()}
)
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
else:
num_others += 1
required_state_types.append((state_type, state_key))
Expand Down Expand Up @@ -1016,8 +1051,8 @@ async def get_room_sync_data(
changed_required_state_map, added_state_filter = (
_required_state_changes(
user.to_string(),
previous_room_config=prev_room_sync_config,
room_sync_config=room_sync_config,
prev_required_state_map=prev_room_sync_config.required_state_map,
request_required_state_map=expanded_required_state_map,
state_deltas=room_state_delta_id_map,
)
)
Expand Down Expand Up @@ -1131,7 +1166,9 @@ async def get_room_sync_data(
# sensible order again.
bump_stamp = 0

room_sync_required_state_map_to_persist = room_sync_config.required_state_map
room_sync_required_state_map_to_persist: Mapping[str, AbstractSet[str]] = (
expanded_required_state_map
)
if changed_required_state_map:
room_sync_required_state_map_to_persist = changed_required_state_map

Expand Down Expand Up @@ -1185,7 +1222,10 @@ async def get_room_sync_data(
)

else:
new_connection_state.room_configs[room_id] = room_sync_config
new_connection_state.room_configs[room_id] = RoomSyncConfig(
timeline_limit=room_sync_config.timeline_limit,
required_state_map=room_sync_required_state_map_to_persist,
)

set_tag(SynapseTags.RESULT_PREFIX + "initial", initial)

Expand Down Expand Up @@ -1320,8 +1360,8 @@ async def _get_bump_stamp(
def _required_state_changes(
user_id: str,
*,
previous_room_config: "RoomSyncConfig",
room_sync_config: RoomSyncConfig,
prev_required_state_map: Mapping[str, AbstractSet[str]],
request_required_state_map: Mapping[str, AbstractSet[str]],
state_deltas: StateMap[str],
) -> Tuple[Optional[Mapping[str, AbstractSet[str]]], StateFilter]:
"""Calculates the changes between the required state room config from the
Expand All @@ -1342,10 +1382,6 @@ def _required_state_changes(
and the state filter to use to fetch extra current state that we need to
return.
"""

prev_required_state_map = previous_room_config.required_state_map
request_required_state_map = room_sync_config.required_state_map

if prev_required_state_map == request_required_state_map:
# There has been no change. Return immediately.
return None, StateFilter.none()
Expand Down Expand Up @@ -1378,12 +1414,19 @@ def _required_state_changes(
# client. Passed to `StateFilter.from_types(...)`
added: List[Tuple[str, Optional[str]]] = []

# Convert the list of state deltas to map from type to state_keys that have
# changed.
changed_types_to_state_keys: Dict[str, Set[str]] = {}
for event_type, state_key in state_deltas:
changed_types_to_state_keys.setdefault(event_type, set()).add(state_key)

# First we calculate what, if anything, has been *added*.
for event_type in (
prev_required_state_map.keys() | request_required_state_map.keys()
):
old_state_keys = prev_required_state_map.get(event_type, set())
request_state_keys = request_required_state_map.get(event_type, set())
changed_state_keys = changed_types_to_state_keys.get(event_type, set())

if old_state_keys == request_state_keys:
# No change to this type
Expand All @@ -1393,8 +1436,21 @@ def _required_state_changes(
# Nothing *added*, so we skip. Removals happen below.
continue

# We only remove state keys from the effective state if they've been
# removed from the request *and* the state has changed. This ensures
# that if a client removes and then re-adds a state key, we only send
# down the associated current state event if its changed (rather than
# sending down the same event twice).
invalidated_state_keys = (
old_state_keys - request_state_keys
) & changed_state_keys

# Always update changes to include the newly added keys
changes[event_type] = request_state_keys
changes[event_type] = request_state_keys | (
# Wildcard and lazy state keys are not sticky from previous requests
(old_state_keys - {StateValues.WILDCARD, StateValues.LAZY})
- invalidated_state_keys
)

if StateValues.WILDCARD in old_state_keys:
# We were previously fetching everything for this type, so we don't need to
Expand All @@ -1421,12 +1477,6 @@ def _required_state_changes(

added_state_filter = StateFilter.from_types(added)

# Convert the list of state deltas to map from type to state_keys that have
# changed.
changed_types_to_state_keys: Dict[str, Set[str]] = {}
for event_type, state_key in state_deltas:
changed_types_to_state_keys.setdefault(event_type, set()).add(state_key)

# Figure out what changes we need to apply to the effective required state
# config.
for event_type, changed_state_keys in changed_types_to_state_keys.items():
Expand All @@ -1437,15 +1487,29 @@ def _required_state_changes(
# No change.
continue

# If we see the `user_id`` as a state_key, also add "$ME" to the list of state
# that has changed to account for people requesting `required_state` with `$ME`
# or their user ID.
if user_id in changed_state_keys:
changed_state_keys.add(StateValues.ME)

# We only remove state keys from the effective state if they've been
# removed from the request *and* the state has changed. This ensures
# that if a client removes and then re-adds a state key, we only send
# down the associated current state event if its changed (rather than
# sending down the same event twice).
invalidated_state_keys = (
old_state_keys - request_state_keys
) & changed_state_keys

if request_state_keys - old_state_keys:
# We've expanded the set of state keys, so we just clobber the
# current set with the new set.
#
# We could also ensure that we keep entries where the state hasn't
# changed, but are no longer in the requested required state, but
# that's a sufficient edge case that we can ignore (as its only a
# performance optimization).
changes[event_type] = request_state_keys
# We've expanded the set of state keys, use the new requested set
# with whatever hasn't been invalidated from the previous set.
changes[event_type] = request_state_keys | (
# Wildcard and lazy state keys are not sticky from previous requests
(old_state_keys - {StateValues.WILDCARD, StateValues.LAZY})
- invalidated_state_keys
)
continue

old_state_key_wildcard = StateValues.WILDCARD in old_state_keys
Expand All @@ -1467,11 +1531,6 @@ def _required_state_changes(
changes[event_type] = request_state_keys
continue

# Handle "$ME" values by adding "$ME" if the state key matches the user
# ID.
if user_id in changed_state_keys:
changed_state_keys.add(StateValues.ME)

# At this point there are no wildcards and no additions to the set of
# state keys requested, only deletions.
#
Expand All @@ -1480,9 +1539,8 @@ def _required_state_changes(
# that if a client removes and then re-adds a state key, we only send
# down the associated current state event if its changed (rather than
# sending down the same event twice).
invalidated = (old_state_keys - request_state_keys) & changed_state_keys
if invalidated:
changes[event_type] = old_state_keys - invalidated
if invalidated_state_keys:
changes[event_type] = old_state_keys - invalidated_state_keys

if changes:
# Update the required state config based on the changes.
Expand Down
Loading
Loading