Skip to content

Commit 72cd5cc

Browse files
Make room upgrades faster for rooms with many bans (#18574)
We do this by a) not pulling out all membership events, and b) batch inserting bans. One blocking concern is that this bypasses the `update_membership` function, which otherwise all other membership events go via. In this case it's fine (having audited what it is doing), but I'm hesitant to set the precedent of bypassing it, given it has a lot of logic in there. --------- Co-authored-by: Eric Eastwood <erice@element.io>
1 parent e16fbdc commit 72cd5cc

5 files changed

Lines changed: 155 additions & 25 deletions

File tree

changelog.d/18574.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speed up upgrading a room with large numbers of banned users.

synapse/handlers/message.py

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import logging
2323
import random
2424
from http import HTTPStatus
25-
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple
25+
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Sequence, Tuple
2626

2727
from canonicaljson import encode_canonical_json
2828

@@ -55,7 +55,11 @@
5555
from synapse.event_auth import validate_event_for_room_version
5656
from synapse.events import EventBase, relation_from_event
5757
from synapse.events.builder import EventBuilder
58-
from synapse.events.snapshot import EventContext, UnpersistedEventContextBase
58+
from synapse.events.snapshot import (
59+
EventContext,
60+
UnpersistedEventContext,
61+
UnpersistedEventContextBase,
62+
)
5963
from synapse.events.utils import SerializeEventConfig, maybe_upsert_event_field
6064
from synapse.events.validator import EventValidator
6165
from synapse.handlers.directory import DirectoryHandler
@@ -66,6 +70,7 @@
6670
from synapse.replication.http.send_events import ReplicationSendEventsRestServlet
6771
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
6872
from synapse.types import (
73+
JsonDict,
6974
PersistedEventPosition,
7075
Requester,
7176
RoomAlias,
@@ -1520,6 +1525,92 @@ async def handle_new_client_event(
15201525

15211526
return result
15221527

1528+
async def create_and_send_new_client_events(
1529+
self,
1530+
requester: Requester,
1531+
room_id: str,
1532+
prev_event_id: str,
1533+
event_dicts: Sequence[JsonDict],
1534+
ratelimit: bool = True,
1535+
ignore_shadow_ban: bool = False,
1536+
) -> None:
1537+
"""Helper to create and send a batch of new client events.
1538+
1539+
This supports sending membership events in very limited circumstances
1540+
(namely that the event is valid as is and doesn't need federation
1541+
requests or anything). Callers should prefer to use `update_membership`,
1542+
which correctly handles membership events in all cases. We allow
1543+
sending membership events here as its useful when copying e.g. bans
1544+
between rooms.
1545+
1546+
All other events and state events are supported.
1547+
1548+
Args:
1549+
requester: The requester sending the events.
1550+
room_id: The room ID to send the events in.
1551+
prev_event_id: The event ID to use as the previous event for the first
1552+
of the events, must have already been persisted.
1553+
event_dicts: A sequence of event dictionaries to create and send.
1554+
ratelimit: Whether to rate limit this send.
1555+
ignore_shadow_ban: True if shadow-banned users should be allowed to
1556+
send these events.
1557+
"""
1558+
1559+
if not event_dicts:
1560+
# Nothing to do.
1561+
return
1562+
1563+
state_groups = await self._storage_controllers.state.get_state_group_for_events(
1564+
[prev_event_id]
1565+
)
1566+
if prev_event_id not in state_groups:
1567+
# This should only happen if we got passed a prev event ID that
1568+
# hasn't been persisted yet.
1569+
raise Exception("Previous event ID not found ")
1570+
1571+
current_state_group = state_groups[prev_event_id]
1572+
state_map = await self._storage_controllers.state.get_state_ids_for_group(
1573+
current_state_group
1574+
)
1575+
1576+
events_and_contexts_to_send = []
1577+
state_map = dict(state_map)
1578+
depth = None
1579+
1580+
for event_dict in event_dicts:
1581+
event, context = await self.create_event(
1582+
requester=requester,
1583+
event_dict=event_dict,
1584+
prev_event_ids=[prev_event_id],
1585+
depth=depth,
1586+
# Take a copy to ensure each event gets a unique copy of
1587+
# state_map since it is modified below.
1588+
state_map=dict(state_map),
1589+
for_batch=True,
1590+
)
1591+
events_and_contexts_to_send.append((event, context))
1592+
1593+
prev_event_id = event.event_id
1594+
depth = event.depth + 1
1595+
if event.is_state():
1596+
# If this is a state event, we need to update the state map
1597+
# so that it can be used for the next event.
1598+
state_map[(event.type, event.state_key)] = event.event_id
1599+
1600+
datastore = self.hs.get_datastores().state
1601+
events_and_context = (
1602+
await UnpersistedEventContext.batch_persist_unpersisted_contexts(
1603+
events_and_contexts_to_send, room_id, current_state_group, datastore
1604+
)
1605+
)
1606+
1607+
await self.handle_new_client_event(
1608+
requester,
1609+
events_and_context,
1610+
ignore_shadow_ban=ignore_shadow_ban,
1611+
ratelimit=ratelimit,
1612+
)
1613+
15231614
async def _persist_events(
15241615
self,
15251616
requester: Requester,

synapse/handlers/room.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
from synapse.types.state import StateFilter
9595
from synapse.util import stringutils
9696
from synapse.util.caches.response_cache import ResponseCache
97+
from synapse.util.iterutils import batch_iter
9798
from synapse.util.stringutils import parse_and_validate_server_name
9899
from synapse.visibility import filter_events_for_client
99100

@@ -607,7 +608,7 @@ async def clone_existing_room(
607608
additional_fields=spam_check[1],
608609
)
609610

610-
await self._send_events_for_new_room(
611+
_, last_event_id, _ = await self._send_events_for_new_room(
611612
requester,
612613
new_room_id,
613614
new_room_version,
@@ -620,29 +621,32 @@ async def clone_existing_room(
620621
)
621622

622623
# Transfer membership events
623-
old_room_member_state_ids = (
624-
await self._storage_controllers.state.get_current_state_ids(
625-
old_room_id, StateFilter.from_types([(EventTypes.Member, None)])
626-
)
627-
)
624+
ban_event_ids = await self.store.get_ban_event_ids_in_room(old_room_id)
625+
if ban_event_ids:
626+
ban_events = await self.store.get_events_as_list(ban_event_ids)
628627

629-
# map from event_id to BaseEvent
630-
old_room_member_state_events = await self.store.get_events(
631-
old_room_member_state_ids.values()
632-
)
633-
for old_event in old_room_member_state_events.values():
634-
# Only transfer ban events
635-
if (
636-
"membership" in old_event.content
637-
and old_event.content["membership"] == "ban"
638-
):
639-
await self.room_member_handler.update_membership(
640-
requester,
641-
UserID.from_string(old_event.state_key),
642-
new_room_id,
643-
"ban",
628+
# Add any banned users to the new room.
629+
#
630+
# Note generally we should send membership events via
631+
# `update_membership`, however in this case its fine to bypass as
632+
# these bans don't need any special treatment, i.e. the sender is in
633+
# the room and they don't need any extra signatures, etc.
634+
for batched_events in batch_iter(ban_events, 1000):
635+
await self.event_creation_handler.create_and_send_new_client_events(
636+
requester=requester,
637+
room_id=new_room_id,
638+
prev_event_id=last_event_id,
639+
event_dicts=[
640+
{
641+
"type": EventTypes.Member,
642+
"state_key": ban_event.state_key,
643+
"room_id": new_room_id,
644+
"sender": requester.user.to_string(),
645+
"content": ban_event.content,
646+
}
647+
for ban_event in batched_events
648+
],
644649
ratelimit=False,
645-
content=old_event.content,
646650
)
647651

648652
# XXX invites/joins

synapse/storage/databases/main/roommember.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1849,6 +1849,19 @@ def _get_room_participation_txn(
18491849
"_get_room_participation_txn", _get_room_participation_txn, user_id, room_id
18501850
)
18511851

1852+
async def get_ban_event_ids_in_room(self, room_id: str) -> StrCollection:
1853+
"""Get all event IDs for ban events in the given room."""
1854+
return await self.db_pool.simple_select_onecol(
1855+
table="current_state_events",
1856+
keyvalues={
1857+
"room_id": room_id,
1858+
"type": EventTypes.Member,
1859+
"membership": Membership.BAN,
1860+
},
1861+
retcol="event_id",
1862+
desc="get_ban_event_ids_in_room",
1863+
)
1864+
18521865

18531866
class RoomMemberBackgroundUpdateStore(SQLBaseStore):
18541867
def __init__(

tests/rest/client/test_upgrade_room.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from twisted.internet.testing import MemoryReactor
2525

26-
from synapse.api.constants import EventContentFields, EventTypes, RoomTypes
26+
from synapse.api.constants import EventContentFields, EventTypes, Membership, RoomTypes
2727
from synapse.config.server import DEFAULT_ROOM_VERSION
2828
from synapse.rest import admin
2929
from synapse.rest.client import login, room, room_upgrade_rest_servlet
@@ -411,3 +411,24 @@ def test_first_upgrade_does_not_block_second(self) -> None:
411411

412412
channel = self._upgrade_room(expire_cache=False)
413413
self.assertEqual(200, channel.code, channel.result)
414+
415+
def test_bans(self) -> None:
416+
"""
417+
Test that bans get copied over when upgrading a room.
418+
"""
419+
420+
users_to_ban = ["@user2:test", "@user3:test", "@user4:test"]
421+
for user in users_to_ban:
422+
self.helper.ban(self.room_id, self.creator, user, tok=self.creator_token)
423+
424+
channel = self._upgrade_room(self.creator_token)
425+
self.assertEqual(200, channel.code, channel.result)
426+
427+
for user in users_to_ban:
428+
content = self.helper.get_state(
429+
self.room_id,
430+
event_type=EventTypes.Member,
431+
state_key=user,
432+
tok=self.creator_token,
433+
)
434+
self.assertEqual(content[EventContentFields.MEMBERSHIP], Membership.BAN)

0 commit comments

Comments
 (0)