Skip to content

Commit

Permalink
Fix deduplicating of membership events to not create unused state gro…
Browse files Browse the repository at this point in the history
…ups. (element-hq#17164)

We try and deduplicate in two places: 1) really early on, and 2) just
before we persist the event. The first case was broken due to it
occuring before the profile information was added, and so it thought the
event contents were different.

The second case did catch it and handle it correctly, however doing so
creates a redundant state group leading to bloat.

Fixes element-hq#3791
  • Loading branch information
erikjohnston authored and H-Shay committed May 31, 2024
1 parent 641b6b2 commit 44bee74
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 35 deletions.
1 change: 1 addition & 0 deletions changelog.d/17164.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix deduplicating of membership events to not create unused state groups.
32 changes: 0 additions & 32 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,13 +496,6 @@ def __init__(self, hs: "HomeServer"):

self.room_prejoin_state_types = self.hs.config.api.room_prejoin_state

self.membership_types_to_include_profile_data_in = {
Membership.JOIN,
Membership.KNOCK,
}
if self.hs.config.server.include_profile_data_on_invite:
self.membership_types_to_include_profile_data_in.add(Membership.INVITE)

self.send_event = ReplicationSendEventRestServlet.make_client(hs)
self.send_events = ReplicationSendEventsRestServlet.make_client(hs)

Expand Down Expand Up @@ -594,8 +587,6 @@ async def create_event(
Creates an FrozenEvent object, filling out auth_events, prev_events,
etc.
Adds display names to Join membership events.
Args:
requester
event_dict: An entire event
Expand Down Expand Up @@ -683,29 +674,6 @@ async def create_event(

self.validator.validate_builder(builder)

if builder.type == EventTypes.Member:
membership = builder.content.get("membership", None)
target = UserID.from_string(builder.state_key)

if membership in self.membership_types_to_include_profile_data_in:
# If event doesn't include a display name, add one.
profile = self.profile_handler
content = builder.content

try:
if "displayname" not in content:
displayname = await profile.get_displayname(target)
if displayname is not None:
content["displayname"] = displayname
if "avatar_url" not in content:
avatar_url = await profile.get_avatar_url(target)
if avatar_url is not None:
content["avatar_url"] = avatar_url
except Exception as e:
logger.info(
"Failed to get profile information for %r: %s", target, e
)

is_exempt = await self._is_exempt_from_privacy_policy(builder, requester)
if require_consent and not is_exempt:
await self.assert_accepted_privacy_policy(requester)
Expand Down
35 changes: 32 additions & 3 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ def __init__(self, hs: "HomeServer"):
self.event_auth_handler = hs.get_event_auth_handler()
self._worker_lock_handler = hs.get_worker_locks_handler()

self._membership_types_to_include_profile_data_in = {
Membership.JOIN,
Membership.KNOCK,
}
if self.hs.config.server.include_profile_data_on_invite:
self._membership_types_to_include_profile_data_in.add(Membership.INVITE)

self.member_linearizer: Linearizer = Linearizer(name="member")
self.member_as_limiter = Linearizer(max_count=10, name="member_as_limiter")

Expand Down Expand Up @@ -785,9 +792,8 @@ async def update_membership_locked(
if (
not self.allow_per_room_profiles and not is_requester_server_notices_user
) or requester.shadow_banned:
# Strip profile data, knowing that new profile data will be added to the
# event's content in event_creation_handler.create_event() using the target's
# global profile.
# Strip profile data, knowing that new profile data will be added to
# the event's content below using the target's global profile.
content.pop("displayname", None)
content.pop("avatar_url", None)

Expand Down Expand Up @@ -823,6 +829,29 @@ async def update_membership_locked(
if action in ["kick", "unban"]:
effective_membership_state = "leave"

if effective_membership_state not in Membership.LIST:
raise SynapseError(400, "Invalid membership key")

# Add profile data for joins etc, if no per-room profile.
if (
effective_membership_state
in self._membership_types_to_include_profile_data_in
):
# If event doesn't include a display name, add one.
profile = self.profile_handler

try:
if "displayname" not in content:
displayname = await profile.get_displayname(target)
if displayname is not None:
content["displayname"] = displayname
if "avatar_url" not in content:
avatar_url = await profile.get_avatar_url(target)
if avatar_url is not None:
content["avatar_url"] = avatar_url
except Exception as e:
logger.info("Failed to get profile information for %r: %s", target, e)

# if this is a join with a 3pid signature, we may need to turn a 3pid
# invite into a normal invite before we can handle the join.
if third_party_signed is not None:
Expand Down
21 changes: 21 additions & 0 deletions tests/handlers/test_room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,24 @@ def test_rejoin_forgotten_by_user(self) -> None:
self.assertFalse(
self.get_success(self.store.did_forget(self.alice, self.room_id))
)

def test_deduplicate_joins(self) -> None:
"""
Test that calling /join multiple times does not store a new state group.
"""

self.helper.join(self.room_id, user=self.bob, tok=self.bob_token)

sql = "SELECT COUNT(*) FROM state_groups WHERE room_id = ?"
rows = self.get_success(
self.store.db_pool.execute("test_deduplicate_joins", sql, self.room_id)
)
initial_count = rows[0][0]

self.helper.join(self.room_id, user=self.bob, tok=self.bob_token)
rows = self.get_success(
self.store.db_pool.execute("test_deduplicate_joins", sql, self.room_id)
)
new_count = rows[0][0]

self.assertEqual(initial_count, new_count)

0 comments on commit 44bee74

Please sign in to comment.