Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Persist CreateRoom events to DB in a batch #13800

Merged
merged 10 commits into from
Sep 28, 2022
1 change: 1 addition & 0 deletions changelog.d/13800.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up creation of DM rooms.
663 changes: 349 additions & 314 deletions synapse/handlers/message.py

Large diffs are not rendered by default.

21 changes: 11 additions & 10 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,7 @@ async def _upgrade_room(
# now send the tombstone
await self.event_creation_handler.handle_new_client_event(
requester=requester,
event=tombstone_event,
context=tombstone_context,
events_and_context=[(tombstone_event, tombstone_context)],
)

state_filter = StateFilter.from_types(
Expand Down Expand Up @@ -1057,8 +1056,10 @@ async def _send_events_for_new_room(
creator_id = creator.user.to_string()
event_keys = {"room_id": room_id, "sender": creator_id, "state_key": ""}
depth = 1

# the last event sent/persisted to the db
last_sent_event_id: Optional[str] = None

# the most recently created event
prev_event: List[str] = []
# a map of event types, state keys -> event_ids. We collect these mappings this as events are
Expand Down Expand Up @@ -1112,8 +1113,7 @@ async def send(

ev = await self.event_creation_handler.handle_new_client_event(
requester=creator,
event=event,
context=context,
events_and_context=[(event, context)],
ratelimit=False,
ignore_shadow_ban=True,
)
Expand Down Expand Up @@ -1152,7 +1152,6 @@ async def send(
prev_event_ids=[last_sent_event_id],
depth=depth,
)
last_sent_event_id = member_event_id
prev_event = [member_event_id]

# update the depth and state map here as the membership event has been created
Expand All @@ -1168,7 +1167,7 @@ async def send(
EventTypes.PowerLevels, pl_content, False
)
current_state_group = power_context._state_group
last_sent_stream_id = await send(power_event, power_context, creator)
await send(power_event, power_context, creator)
else:
power_level_content: JsonDict = {
"users": {creator_id: 100},
Expand Down Expand Up @@ -1217,7 +1216,7 @@ async def send(
False,
)
current_state_group = pl_context._state_group
last_sent_stream_id = await send(pl_event, pl_context, creator)
await send(pl_event, pl_context, creator)

events_to_send = []
if room_alias and (EventTypes.CanonicalAlias, "") not in initial_state:
Expand Down Expand Up @@ -1271,9 +1270,11 @@ async def send(
)
events_to_send.append((encryption_event, encryption_context))

for event, context in events_to_send:
last_sent_stream_id = await send(event, context, creator)
return last_sent_stream_id, last_sent_event_id, depth
last_event = await self.event_creation_handler.handle_new_client_event(
creator, events_to_send, ignore_shadow_ban=True
)
assert last_event.internal_metadata.stream_ordering is not None
return last_event.internal_metadata.stream_ordering, last_event.event_id, depth

def _generate_room_id(self) -> str:
"""Generates a random room ID.
Expand Down
3 changes: 1 addition & 2 deletions synapse/handlers/room_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,7 @@ async def persist_historical_events(
await self.create_requester_for_user_id_from_app_service(
event.sender, app_service_requester.app_service
),
event=event,
context=context,
events_and_context=[(event, context)],
)

return event_ids
Expand Down
11 changes: 6 additions & 5 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,8 +432,7 @@ async def _local_membership_update(
with opentracing.start_active_span("handle_new_client_event"):
result_event = await self.event_creation_handler.handle_new_client_event(
requester,
event,
context,
events_and_context=[(event, context)],
extra_users=[target],
ratelimit=ratelimit,
)
Expand Down Expand Up @@ -1252,7 +1251,10 @@ async def send_membership_event(
raise SynapseError(403, "This room has been blocked on this server")

event = await self.event_creation_handler.handle_new_client_event(
requester, event, context, extra_users=[target_user], ratelimit=ratelimit
requester,
events_and_context=[(event, context)],
extra_users=[target_user],
ratelimit=ratelimit,
)

prev_member_event_id = prev_state_ids.get(
Expand Down Expand Up @@ -1860,8 +1862,7 @@ async def _generate_local_out_of_band_leave(

result_event = await self.event_creation_handler.handle_new_client_event(
requester,
event,
context,
events_and_context=[(event, context)],
extra_users=[UserID.from_string(target_user)],
)
# we know it was persisted, so must have a stream ordering
Expand Down
2 changes: 2 additions & 0 deletions synapse/replication/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
push,
register,
send_event,
send_events,
state,
streams,
)
Expand All @@ -43,6 +44,7 @@ def __init__(self, hs: "HomeServer"):

def register_servlets(self, hs: "HomeServer") -> None:
send_event.register_servlets(hs, self)
send_events.register_servlets(hs, self)
federation.register_servlets(hs, self)
presence.register_servlets(hs, self)
membership.register_servlets(hs, self)
Expand Down
4 changes: 2 additions & 2 deletions synapse/replication/http/send_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ async def _handle_request( # type: ignore[override]
"Got event to send with ID: %s into room: %s", event.event_id, event.room_id
)

event = await self.event_creation_handler.persist_and_notify_client_event(
requester, event, context, ratelimit=ratelimit, extra_users=extra_users
event = await self.event_creation_handler.persist_and_notify_client_events(
requester, [(event, context)], ratelimit=ratelimit, extra_users=extra_users
)

return (
Expand Down
171 changes: 171 additions & 0 deletions synapse/replication/http/send_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Copyright 2022 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from typing import TYPE_CHECKING, List, Tuple

from twisted.web.server import Request

from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.events import EventBase, make_event_from_dict
from synapse.events.snapshot import EventContext
from synapse.http.server import HttpServer
from synapse.http.servlet import parse_json_object_from_request
from synapse.replication.http._base import ReplicationEndpoint
from synapse.types import JsonDict, Requester, UserID
from synapse.util.metrics import Measure

if TYPE_CHECKING:
from synapse.server import HomeServer
from synapse.storage.databases.main import DataStore

logger = logging.getLogger(__name__)


class ReplicationSendEventsRestServlet(ReplicationEndpoint):
"""Handles batches of newly created events on workers, including persisting and
notifying.

The API looks like:

POST /_synapse/replication/send_events/:txn_id

{
"events": [{
"event": { .. serialized event .. },
"room_version": .., // "1", "2", "3", etc: the version of the room
// containing the event
"event_format_version": .., // 1,2,3 etc: the event format version
"internal_metadata": { .. serialized internal_metadata .. },
"outlier": true|false,
"rejected_reason": .., // The event.rejected_reason field
"context": { .. serialized event context .. },
"requester": { .. serialized requester .. },
"ratelimit": true,
}]
}

200 OK

{ "stream_id": 12345, "event_id": "$abcdef..." }

Responds with a 409 when a `PartialStateConflictError` is raised due to an event
context that needs to be recomputed due to the un-partial stating of a room.

"""

NAME = "send_events"
PATH_ARGS = ()

def __init__(self, hs: "HomeServer"):
super().__init__(hs)

self.event_creation_handler = hs.get_event_creation_handler()
self.store = hs.get_datastores().main
self._storage_controllers = hs.get_storage_controllers()
self.clock = hs.get_clock()

@staticmethod
async def _serialize_payload( # type: ignore[override]
events_and_context: List[Tuple[EventBase, EventContext]],
store: "DataStore",
requester: Requester,
ratelimit: bool,
extra_users: List[UserID],
) -> JsonDict:
"""
Args:
store
requester
events_and_ctx
ratelimit
"""
serialized_events = []

for event, context in events_and_context:
serialized_context = await context.serialize(event, store)
serialized_event = {
"event": event.get_pdu_json(),
"room_version": event.room_version.identifier,
"event_format_version": event.format_version,
"internal_metadata": event.internal_metadata.get_dict(),
"outlier": event.internal_metadata.is_outlier(),
"rejected_reason": event.rejected_reason,
"context": serialized_context,
"requester": requester.serialize(),
"ratelimit": ratelimit,
"extra_users": [u.to_string() for u in extra_users],
}
serialized_events.append(serialized_event)

payload = {"events": serialized_events}

return payload

async def _handle_request( # type: ignore[override]
self, request: Request
) -> Tuple[int, JsonDict]:
with Measure(self.clock, "repl_send_events_parse"):
payload = parse_json_object_from_request(request)
events_and_context = []
events = payload["events"]

for event_payload in events:
event_dict = event_payload["event"]
room_ver = KNOWN_ROOM_VERSIONS[event_payload["room_version"]]
internal_metadata = event_payload["internal_metadata"]
rejected_reason = event_payload["rejected_reason"]

event = make_event_from_dict(
event_dict, room_ver, internal_metadata, rejected_reason
)
event.internal_metadata.outlier = event_payload["outlier"]

requester = Requester.deserialize(
self.store, event_payload["requester"]
)
context = EventContext.deserialize(
self._storage_controllers, event_payload["context"]
)

ratelimit = event_payload["ratelimit"]
events_and_context.append((event, context))

extra_users = [
UserID.from_string(u) for u in event_payload["extra_users"]
]

logger.info(
"Got batch of events to send, last ID of batch is: %s, sending into room: %s",
event.event_id,
event.room_id,
)

last_event = (
await self.event_creation_handler.persist_and_notify_client_events(
requester, events_and_context, ratelimit, extra_users
)
)

return (
200,
{
"stream_id": last_event.internal_metadata.stream_ordering,
"event_id": last_event.event_id,
},
)


def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
ReplicationSendEventsRestServlet(hs).register(http_server)
10 changes: 8 additions & 2 deletions tests/handlers/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ def test_duplicated_txn_id(self):
event1, context = self._create_duplicate_event(txn_id)

ret_event1 = self.get_success(
self.handler.handle_new_client_event(self.requester, event1, context)
self.handler.handle_new_client_event(
self.requester,
events_and_context=[(event1, context)],
)
)
stream_id1 = ret_event1.internal_metadata.stream_ordering

Expand All @@ -118,7 +121,10 @@ def test_duplicated_txn_id(self):
self.assertNotEqual(event1.event_id, event2.event_id)

ret_event2 = self.get_success(
self.handler.handle_new_client_event(self.requester, event2, context)
self.handler.handle_new_client_event(
self.requester,
events_and_context=[(event2, context)],
)
)
stream_id2 = ret_event2.internal_metadata.stream_ordering

Expand Down
4 changes: 3 additions & 1 deletion tests/handlers/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,9 @@ def test_auto_create_auto_join_room_preset_invalid_permissions(self):
)
)
self.get_success(
event_creation_handler.handle_new_client_event(requester, event, context)
event_creation_handler.handle_new_client_event(
requester, events_and_context=[(event, context)]
)
)

# Register a second user, which won't be be in the room (or even have an invite)
Expand Down
8 changes: 6 additions & 2 deletions tests/storage/test_event_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,9 @@ def _generate_room(self) -> Tuple[str, List[Set[str]]]:
)
)
self.get_success(
event_handler.handle_new_client_event(self.requester, event, context)
event_handler.handle_new_client_event(
self.requester, events_and_context=[(event, context)]
)
)
state1 = set(self.get_success(context.get_current_state_ids()).values())

Expand All @@ -549,7 +551,9 @@ def _generate_room(self) -> Tuple[str, List[Set[str]]]:
)
)
self.get_success(
event_handler.handle_new_client_event(self.requester, event, context)
event_handler.handle_new_client_event(
self.requester, events_and_context=[(event, context)]
)
)
state2 = set(self.get_success(context.get_current_state_ids()).values())

Expand Down
4 changes: 3 additions & 1 deletion tests/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,9 @@ def create_and_send_event(
event.internal_metadata.soft_failed = True

self.get_success(
event_creator.handle_new_client_event(requester, event, context)
event_creator.handle_new_client_event(
requester, events_and_context=[(event, context)]
)
)

return event.event_id
Expand Down