Build a new room's events in one Rust call

Behind `experimental_features.rust_room_creation: on`, the room handler
collects the events that follow the create event (the creator's join,
power levels, join rules, history visibility, the initial state, name,
topic) as `(type, state_key, content)`, and `LocalEventSigner` builds,
hashes and signs them all in one call. Each event's prev event, depth
and auth events follow from the one before, as `create_event` gives
them for a batch.

Python keeps every check: the requester checks ran when the create
event was built, and each built event still goes through
`validate_builder`, `validate_new` and the relation check before
joining the batch, which persists through `handle_new_client_event` as
before. Invites are still built by `create_event`.

Rooms the Rust builder does not cover are built in Python and counted
as fallbacks: v1 and v2 rooms, MSC4242 state DAG rooms, appservice
senders, modules with `check_event_allowed`, initial `m.room.aliases`
events, and `m.call.invite` or `m.room.create` in the initial state. If
Rust fails, the events are built in Python. If Python rejects them too,
the request was bad and gets Python's error. Otherwise it counts as a
Rust error. In tests,
`SYNAPSE_TEST_RUST_ROOM_CREATION=on` turns the builder on and makes a
failure fail the test.

Python orders auth events by iterating a set, so the order changes with
the process's hash seed. Rust keeps the order of the auth types. A test
creates rooms of several configurations and room versions in Python,
rebuilds their events in Rust from the same inputs, and compares them
with the auth events taken as a set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVLsNh6akt4ifEizY4D1ta
This commit is contained in:
Erik JohnstonandClaude Opus 5.5 committed 2026-10-18 17:45:44 +00:00
1 parent 9b6e901fa5
commit ef18e9ebc7
8 files changed
+741 -59

No files matched your search

+161 -12
View File
@@ -19,7 +19,10 @@
//! redacted event with the server's key, and parse the result as an
//! [`Event`].
use std::sync::Arc;
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use anyhow::{bail, Context};
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
@@ -38,7 +41,8 @@ use super::{
Event,
};
use crate::{
canonical_json::{to_vec_canonical, CanonicalizationOptions},
canonical_json::{to_vec_canonical, CanonicalizationOptions, MAX_VALID_INTEGER},
event_auth::auth_types_for,
federation::signing::FederationSigningKey,
room_versions::{EventFormatVersions, RoomVersion},
};
@@ -73,29 +77,174 @@ impl LocalEventSigner {
room_version: &Bound<'_, PyAny>,
internal_metadata_dict: &Bound<'_, PyDict>,
) -> PyResult<Event> {
let room_version: &'static RoomVersion = room_version
.getattr("identifier")?
.extract::<&str>()?
.parse()
.map_err(|e| PyValueError::new_err(format!("Unsupported room version: {e}")))?;
let room_version = room_version_from_py(room_version)?;
let internal_metadata = EventInternalMetadata::new(internal_metadata_dict)?;
self.sign(event_json, room_version, internal_metadata)
let event: Map<String, Value> = serde_json::from_slice(event_json)
.context("Failed to parse event JSON")
.map_err(|err| PyValueError::new_err(format!("{err:#}")))?;
self.sign(event, room_version, internal_metadata)
.map_err(|err| PyValueError::new_err(format!("{err:#}")))
}
/// Build the state events that follow the create event of a new room, as
/// the room handler's batch builds them one by one: each event's prev
/// event is the one before it, its depth one more, and its auth events
/// are picked from the state before it, which starts as `state_map`.
///
/// `specs` are `(type, state_key, content)`, with the content as its
/// canonical JSON. Every event gets `origin_server_ts` and a fresh copy
/// of `internal_metadata_dict`.
///
/// Only for room versions whose events refer to other events by ID
/// (not v1 and v2) and that have no state DAG (MSC4242). Raises
/// `ValueError` for anything it cannot build, and the caller builds the
/// events in Python instead.
#[pyo3(signature = (
room_version, room_id, sender, prev_event_id, depth, state_map, specs,
origin_server_ts, internal_metadata_dict,
))]
#[allow(clippy::too_many_arguments)]
fn build_room_events(
&self,
room_version: &Bound<'_, PyAny>,
room_id: &str,
sender: &str,
prev_event_id: String,
depth: i64,
state_map: HashMap<(String, String), String>,
specs: Vec<(String, String, Vec<u8>)>,
origin_server_ts: i64,
internal_metadata_dict: &Bound<'_, PyDict>,
) -> PyResult<Vec<Event>> {
let room_version = room_version_from_py(room_version)?;
let to_py = |err: anyhow::Error| PyValueError::new_err(format!("{err:#}"));
if room_version.event_format == EventFormatVersions::ROOM_V1_V2
|| room_version.msc4242_state_dags
{
return Err(PyValueError::new_err(format!(
"Cannot build a room batch for room version {room_version}"
)));
}
let mut state_map = state_map;
let mut prev_event_id = prev_event_id;
let mut events = Vec::with_capacity(specs.len());
for (depth, (event_type, state_key, content_json)) in (depth..).zip(specs) {
let internal_metadata = EventInternalMetadata::new(internal_metadata_dict)?;
let content: Map<String, Value> = serde_json::from_slice(&content_json)
.context("Failed to parse event content")
.map_err(to_py)?;
let auth_event_ids = room_batch_auth_event_ids(
room_version,
room_id,
&event_type,
sender,
&state_key,
&content,
&state_map,
)
.map_err(to_py)?;
let mut unsigned = Map::new();
unsigned.insert(AGE_TS.to_owned(), Value::from(origin_server_ts));
let mut event = Map::new();
event.insert("auth_events".to_owned(), Value::from(auth_event_ids));
event.insert(
"prev_events".to_owned(),
Value::from(vec![prev_event_id.clone()]),
);
event.insert("type".to_owned(), Value::from(event_type.clone()));
event.insert("sender".to_owned(), Value::from(sender));
event.insert("content".to_owned(), Value::Object(content));
event.insert(UNSIGNED.to_owned(), Value::Object(unsigned));
// Python caps the depth of new events, so other servers accept
// them.
event.insert(
"depth".to_owned(),
Value::from(depth.min(MAX_VALID_INTEGER)),
);
event.insert("room_id".to_owned(), Value::from(room_id));
event.insert("state_key".to_owned(), Value::from(state_key.clone()));
event.insert("origin_server_ts".to_owned(), Value::from(origin_server_ts));
event.insert(SIGNATURES.to_owned(), Value::Object(Map::new()));
let event = self
.sign(event, room_version, internal_metadata)
.map_err(to_py)?;
prev_event_id = event.event_id().to_owned();
state_map.insert((event_type, state_key), prev_event_id.clone());
events.push(event);
}
Ok(events)
}
}
/// The room version Python passes, by its identifier.
fn room_version_from_py(room_version: &Bound<'_, PyAny>) -> PyResult<&'static RoomVersion> {
room_version
.getattr("identifier")?
.extract::<&str>()?
.parse()
.map_err(|e| PyValueError::new_err(format!("Unsupported room version: {e}")))
}
/// The auth events of a new state event, as `compute_auth_events` and the
/// builder pick them: the events in `state_map` for the auth types of the
/// event, in that order. In MSC4291 rooms the create event is implied by the
/// room ID, so it is left out.
fn room_batch_auth_event_ids(
room_version: &RoomVersion,
room_id: &str,
event_type: &str,
sender: &str,
state_key: &str,
content: &Map<String, Value>,
state_map: &HashMap<(String, String), String>,
) -> anyhow::Result<Vec<String>> {
let content: BTreeMap<Box<str>, Value> = content
.iter()
.map(|(key, value)| (Box::from(key.as_str()), value.clone()))
.collect();
let auth_types = auth_types_for(room_version, event_type, sender, Some(state_key), &content)
.map_err(|err| anyhow::anyhow!("{err}"))?;
let mut auth_event_ids: Vec<String> = auth_types
.into_iter()
.filter_map(|(auth_type, auth_state_key)| {
state_map
.get(&(auth_type.to_owned(), auth_state_key.to_owned()))
.filter(|event_id| !event_id.is_empty())
.cloned()
})
.collect();
if room_version.msc4291_room_ids_as_hashes {
let create_event_id = format!("${}", &room_id[1..]);
let Some(index) = auth_event_ids.iter().position(|id| *id == create_event_id) else {
bail!("the create event {create_event_id} is not in the state");
};
auth_event_ids.remove(index);
}
Ok(auth_event_ids)
}
impl LocalEventSigner {
fn sign(
&self,
event_json: &[u8],
mut event: Map<String, Value>,
room_version: &'static RoomVersion,
internal_metadata: EventInternalMetadata,
) -> anyhow::Result<Event> {
let mut event: Map<String, Value> =
serde_json::from_slice(event_json).context("Failed to parse event JSON")?;
let reference_hash = add_hashes_and_signature(
&mut event,
room_version,
+18
View File
@@ -399,6 +399,24 @@ class ExperimentalConfig(Config):
)
self.rust_sliding_sync: Literal["off", "on"] = rust_sliding_sync
# Whether a new room's events after its create event are built, hashed
# and signed in one Rust call, rather than one Python `create_event`
# each. Python still runs every check and persists them. Rooms Rust
# cannot build (v1 and v2, MSC4242 state DAGs, appservice senders,
# `check_event_allowed` modules, `m.room.aliases` in the initial
# state) are built in Python.
rust_room_creation = experimental.get("rust_room_creation", "off")
if rust_room_creation is True:
rust_room_creation = "on"
elif rust_room_creation is False:
rust_room_creation = "off"
if rust_room_creation not in ("off", "on"):
raise ConfigError(
"experimental_features.rust_room_creation must be 'off' or 'on'",
("experimental", "rust_room_creation"),
)
self.rust_room_creation: Literal["off", "on"] = rust_room_creation
# How much of outbound federation runs in Rust. One ordered level, each
# implying the ones before it:
# - "off": all Python.
+186 -44
View File
@@ -39,6 +39,8 @@ from typing import (
)
import attr
from canonicaljson import encode_canonical_json
from prometheus_client import Counter
import synapse.events.snapshot
from synapse.api.constants import (
@@ -79,8 +81,10 @@ from synapse.events.utils import (
copy_and_fixup_power_levels_contents,
)
from synapse.handlers.relations import BundledAggregations
from synapse.metrics import SERVER_NAME_LABEL
from synapse.rest.admin._base import assert_user_is_admin
from synapse.streams import EventSource
from synapse.synapse_rust.events import EventInternalMetadata
from synapse.types import (
JsonDict,
JsonMapping,
@@ -116,6 +120,23 @@ id_server_scheme = "https://"
FIVE_MINUTES_IN_MS = 5 * 60 * 1000
# Event types whose creation runs checks the Rust room builder leaves to
# `create_event`: `m.call.invite` against the database, and `m.room.create`,
# which cannot be sent again. (`m.room.aliases` in the initial state
# already has the create event and the join persisted on their own, which the
# Rust builder does not cover.)
_NOT_BUILT_IN_RUST = frozenset({EventTypes.CallInvite, EventTypes.Create})
# New rooms in `rust_room_creation: on` mode. `result` is "rust" for a room
# whose events after the create event Rust built, "fallback" for one Rust does
# not cover, or "error" for one Python built after Rust failed on it.
rust_room_creation_counter = Counter(
"synapse_rust_room_creation_total",
"Rooms created in `rust_room_creation: on` mode, by whether Rust built "
"their initial events",
labelnames=["result", SERVER_NAME_LABEL],
)
@attr.s(slots=True, frozen=True, auto_attribs=True)
class EventContext:
@@ -129,6 +150,10 @@ class EventContext:
class RoomCreationHandler:
# Tests set this, so that a Rust failure fails the test rather than falling
# back to Python.
_rust_room_creation_strict = False
def __init__(self, hs: "HomeServer"):
self.server_name = hs.hostname
self.store = hs.get_datastores().main
@@ -142,6 +167,8 @@ class RoomCreationHandler:
self.room_member_handler = hs.get_room_member_handler()
self._event_auth_handler = hs.get_event_auth_handler()
self.config = hs.config
self._rust_room_creation = hs.config.experimental.rust_room_creation == "on"
self._event_signer = hs.get_event_builder_factory().signer
self.common_request_ratelimiter = hs.get_request_ratelimiter()
self.creation_ratelimiter = Ratelimiter(
store=self.store,
@@ -1702,6 +1729,90 @@ class RoomCreationHandler:
)
assert not (persist_create_and_join_alone and batched_invites)
# In `rust_room_creation: on` mode, the events after the create event
# are collected here as `(type, state_key, content)`, then built in one
# Rust call by `build_collected_events`. None when each is built in
# Python as it comes.
collected: list[tuple[str, str, JsonMapping]] | None = None
if self._rust_room_creation:
if self._rust_room_creation_unsupported(
creator, room_version, persist_create_and_join_alone
):
rust_room_creation_counter.labels(
result="fallback", **{SERVER_NAME_LABEL: self.hs.hostname}
).inc()
else:
collected = []
async def add_event(
etype: str, content: JsonMapping, state_key: str = ""
) -> None:
"""Build the next event of the batch, or collect it for Rust."""
if collected is not None:
collected.append((etype, state_key, content))
return
event, context = await create_event(
etype, content, True, state_key=state_key
)
events_to_send.append((event, context))
async def build_collected_events() -> None:
"""Build the collected events and add them to the batch, in Rust, or
in Python if Rust cannot build them."""
nonlocal collected, depth, prev_event
specs, collected = collected, None
if not specs:
return
if any(etype in _NOT_BUILT_IN_RUST for etype, _, _ in specs):
rust_room_creation_counter.labels(
result="fallback", **{SERVER_NAME_LABEL: self.hs.hostname}
).inc()
for etype, state_key, content in specs:
await add_event(etype, content, state_key=state_key)
return
try:
events = self._build_room_events_in_rust(
creator, room_version, room_id, prev_event, depth, state_map, specs
)
except Exception as rust_error:
# Build them in Python. If Python rejects them too, the request
# was bad, and Python's error is the one to give. Otherwise Rust
# got it wrong.
for etype, state_key, content in specs:
await add_event(etype, content, state_key=state_key)
logger.error(
"Failed to build the events of room %s in Rust",
room_id,
exc_info=rust_error,
)
rust_room_creation_counter.labels(
result="error", **{SERVER_NAME_LABEL: self.hs.hostname}
).inc()
if self._rust_room_creation_strict:
raise
return
rust_room_creation_counter.labels(
result="rust", **{SERVER_NAME_LABEL: self.hs.hostname}
).inc()
# The checks `create_event` runs on each event it builds. Those on
# the requester ran when it built the create event.
for event in events:
await self._check_event_built_in_rust(event)
context = await self.hs.get_state_handler().calculate_context_info(
event,
state_ids_before_event=dict(state_map),
partial_state=False,
)
events_to_send.append((event, context))
depth += 1
prev_event = [event.event_id]
state_map[(event.type, event.state_key)] = event.event_id
if creation_event_with_context is None:
# MSC2175 removes the creator field from the create event.
if not room_version.implicit_room_creator:
@@ -1729,10 +1840,7 @@ class RoomCreationHandler:
join_content = await self.room_member_handler.build_creator_join_content(
creator, creator_join_profile
)
join_event, join_context = await create_event(
EventTypes.Member, join_content, True, state_key=creator_id
)
events_to_send.append((join_event, join_context))
await add_event(EventTypes.Member, join_content, state_key=creator_id)
if persist_create_and_join_alone:
await persist_batch()
@@ -1741,10 +1849,7 @@ class RoomCreationHandler:
# If the `initial_state` has `EventTypes.PowerLevels` content, use it.
pl_content = initial_state.pop((EventTypes.PowerLevels, ""), None)
if pl_content is not None:
power_event, power_context = await create_event(
EventTypes.PowerLevels, pl_content, True
)
events_to_send.append((power_event, power_context))
await add_event(EventTypes.PowerLevels, pl_content)
else:
# If the `initial_state` does not have `EventTypes.PowerLevels` content,
# use the default power level content.
@@ -1803,49 +1908,33 @@ class RoomCreationHandler:
# apply those. Supplied room override wins over server config.
if power_level_content_override:
power_level_content.update(power_level_content_override)
pl_event, pl_context = await create_event(
EventTypes.PowerLevels,
power_level_content,
True,
)
events_to_send.append((pl_event, pl_context))
await add_event(EventTypes.PowerLevels, power_level_content)
if room_alias and (EventTypes.CanonicalAlias, "") not in initial_state:
room_alias_event, room_alias_context = await create_event(
EventTypes.CanonicalAlias, {"alias": room_alias.to_string()}, True
await add_event(
EventTypes.CanonicalAlias, {"alias": room_alias.to_string()}
)
events_to_send.append((room_alias_event, room_alias_context))
if (EventTypes.JoinRules, "") not in initial_state:
join_rules_event, join_rules_context = await create_event(
EventTypes.JoinRules,
{"join_rule": preset_config["join_rules"]},
True,
await add_event(
EventTypes.JoinRules, {"join_rule": preset_config["join_rules"]}
)
events_to_send.append((join_rules_event, join_rules_context))
if (EventTypes.RoomHistoryVisibility, "") not in initial_state:
visibility_event, visibility_context = await create_event(
await add_event(
EventTypes.RoomHistoryVisibility,
{"history_visibility": preset_config["history_visibility"]},
True,
)
events_to_send.append((visibility_event, visibility_context))
if preset_config["guest_can_join"]:
if (EventTypes.GuestAccess, "") not in initial_state:
guest_access_event, guest_access_context = await create_event(
await add_event(
EventTypes.GuestAccess,
{EventContentFields.GUEST_ACCESS: GuestAccess.CAN_JOIN},
True,
)
events_to_send.append((guest_access_event, guest_access_context))
for (etype, state_key), content in initial_state.items():
event, context = await create_event(
etype, content, True, state_key=state_key
)
events_to_send.append((event, context))
await add_event(etype, content, state_key=state_key)
# If the client supplied its own `m.room.encryption` event in the
# initial state, only let it take precedence over the forced default
@@ -1862,26 +1951,19 @@ class RoomCreationHandler:
and not ignore_forced_encryption
and not supplied_encryption_is_valid
):
encryption_event, encryption_context = await create_event(
await add_event(
EventTypes.RoomEncryption,
{"algorithm": RoomEncryptionAlgorithms.DEFAULT},
True,
state_key="",
)
events_to_send.append((encryption_event, encryption_context))
if "name" in room_config:
name = room_config["name"]
name_event, name_context = await create_event(
EventTypes.Name,
{"name": name},
True,
)
events_to_send.append((name_event, name_context))
await add_event(EventTypes.Name, {"name": name})
if "topic" in room_config:
topic = room_config["topic"]
topic_event, topic_context = await create_event(
await add_event(
EventTypes.Topic,
{
EventContentFields.TOPIC: topic,
@@ -1890,9 +1972,9 @@ class RoomCreationHandler:
EventContentFields.M_TEXT: [{MTextFields.BODY: topic}]
},
},
True,
)
events_to_send.append((topic_event, topic_context))
await build_collected_events()
for invitee, invite_content in batched_invites:
# Check the invite against the auth rules before it joins the batch:
@@ -1927,6 +2009,66 @@ class RoomCreationHandler:
len(invited_users),
)
def _rust_room_creation_unsupported(
self,
creator: Requester,
room_version: RoomVersion,
persist_create_and_join_alone: bool,
) -> bool:
"""Whether a new room's events must be built in Python even in
`rust_room_creation: on` mode.
The Rust builder needs the create event and the join in the batch, refers
to events by ID only, and has no state DAG. Events sent by an appservice
carry it in their context.
"""
return (
persist_create_and_join_alone
or room_version.msc4242_state_dags
or creator.app_service_id is not None
)
def _build_room_events_in_rust(
self,
creator: Requester,
room_version: RoomVersion,
room_id: str,
prev_event: list[str],
depth: int,
state_map: StateMap[str],
specs: list[tuple[str, str, JsonMapping]],
) -> list[EventBase]:
"""Build, hash and sign the events of `specs` in one Rust call, as
`create_event` would build each in turn after `prev_event`."""
# The internal metadata `create_event` gives each event it builds.
internal_metadata = EventInternalMetadata({})
if creator.device_id is not None:
internal_metadata.device_id = creator.device_id
internal_metadata.outlier = False
(prev_event_id,) = prev_event
return self._event_signer.build_room_events(
room_version,
room_id,
creator.user.to_string(),
prev_event_id,
depth,
dict(state_map),
[
(etype, state_key, encode_canonical_json(content))
for etype, state_key, content in specs
],
int(self.clock.time_msec()),
internal_metadata.get_dict(),
)
async def _check_event_built_in_rust(self, event: EventBase) -> None:
"""The checks `create_event` runs on an event of a new room once built."""
validator = self.event_creation_handler.validator
validator.validate_builder(event)
validator.validate_new(event, self.config)
await self.event_creation_handler._validate_event_relation(event)
def _persist_create_and_join_alone(
self, room_version: RoomVersion, initial_state: StateMap[Any]
) -> bool:
+25
View File
@@ -558,3 +558,28 @@ class LocalEventSigner:
Raises:
ValueError: if the JSON is not a valid event for the room version.
"""
def build_room_events(
self,
room_version: RoomVersion,
room_id: str,
sender: str,
prev_event_id: str,
depth: int,
state_map: dict[tuple[str, str], str],
specs: list[tuple[str, str, bytes]],
origin_server_ts: int,
internal_metadata_dict: JsonDict,
) -> list[Event]:
"""Build the state events that follow the create event of a new room,
as the room handler's batch builds them one by one: each event's prev
event is the one before it, its depth one more, and its auth events are
picked from the state before it, which starts as `state_map`.
`specs` are `(type, state_key, content)`, with the content as its
canonical JSON. Every event gets `origin_server_ts` and its own copy of
`internal_metadata_dict`.
Raises:
ValueError: for anything it cannot build, including v1 and v2 rooms
and MSC4242 state DAG rooms. Build the events in Python then.
"""
+322
View File
@@ -0,0 +1,322 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
"""`rust_room_creation`: a new room's events built in one Rust call."""
from typing import Any
from unittest.mock import Mock, patch
from twisted.internet.testing import MemoryReactor
import synapse
from synapse.api.constants import EventTypes
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.events import EventBase
from synapse.handlers.room import rust_room_creation_counter
from synapse.metrics import SERVER_NAME_LABEL
from synapse.rest.client import login, room
from synapse.server import HomeServer
from synapse.types import JsonDict, create_requester
from synapse.util.clock import Clock
from tests import unittest
from tests.unittest import override_config
from tests.utils import RUST_ROOM_CREATION_FOR_TESTS
# Room configurations that between them create every kind of event the room
# handler sends before any invites.
ROOM_CONFIGS: list[JsonDict] = [
{},
{"preset": "public_chat", "name": "A room", "topic": "A topic"},
{"preset": "trusted_private_chat", "invite": ["@bob:test"]},
{
"initial_state": [
{"type": "m.room.guest_access", "content": {"guest_access": "can_join"}},
{"type": "m.space.child", "state_key": "!a:test", "content": {"via": []}},
{
"type": "m.room.power_levels",
"content": {"users_default": 100, "ban": 50},
},
{"type": "m.room.encryption", "content": {"algorithm": "m.megolm"}},
],
"room_alias_name": "alias",
},
{
"power_level_content_override": {"events_default": 10},
"creation_content": {"m.federate": False},
"is_direct": True,
},
]
# Room versions the Rust builder covers, one per event format.
ROOM_VERSIONS = ["3", "10", "11", "12"]
def _comparable(event: EventBase, keys: dict[str, tuple[str, str]]) -> JsonDict:
"""The event's PDU, with the events it refers to given by their `(type,
state_key)` in `keys`, and without its hashes, signatures and ID.
Python orders the auth events by iterating a set of `(type, state_key)`,
which changes with the process's hash seed, so the IDs, which cover that
order, cannot be matched. The order means nothing to the protocol, so the
auth events are compared as a set.
"""
pdu = event.get_pdu_json()
pdu["auth_events"] = sorted(keys[e] for e in pdu["auth_events"])
pdu["prev_events"] = [keys[e] for e in pdu["prev_events"]]
for key in ("hashes", "signatures", "event_id"):
pdu.pop(key, None)
return pdu
def _rust_rooms(hs: HomeServer, result: str) -> float:
return rust_room_creation_counter.labels(
result=result, **{SERVER_NAME_LABEL: hs.hostname}
)._value.get()
class RustRoomCreationTestCase(unittest.HomeserverTestCase):
if RUST_ROOM_CREATION_FOR_TESTS == "off":
skip = "SYNAPSE_TEST_RUST_ROOM_CREATION turns the Rust room builder off"
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
room.register_servlets,
]
def default_config(self) -> JsonDict:
config = super().default_config()
config.setdefault("experimental_features", {}).setdefault(
"rust_room_creation", "on"
)
return config
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.user_id = self.register_user("alice", "pass")
self.tok = self.login("alice", "pass", device_id="DEVICE")
self.register_user("bob", "pass")
self.handler = hs.get_room_creation_handler()
# Strict, whatever SYNAPSE_TEST_RUST_ROOM_CREATION_STRICT says.
self.handler._rust_room_creation_strict = True
self.store = hs.get_datastores().main
def _create_room(self, room_version: str, config: JsonDict) -> str:
body = dict(config)
body["room_version"] = room_version
if "room_alias_name" in body:
# Aliases are unique.
self._aliases = getattr(self, "_aliases", 0) + 1
body["room_alias_name"] += str(self._aliases)
channel = self.make_request("POST", "/createRoom", body, access_token=self.tok)
self.assertEqual(channel.code, 200, channel.json_body)
return channel.json_body["room_id"]
def _room_events(self, room_id: str) -> list[EventBase]:
"""The room's events in the order they were sent."""
rows = self.get_success(
self.store.db_pool.simple_select_list(
"events", {"room_id": room_id}, ("event_id", "stream_ordering")
)
)
event_ids = [event_id for event_id, _ in sorted(rows, key=lambda r: r[1])]
return self.get_success(self.store.get_events_as_list(event_ids))
def test_same_events_as_python(self) -> None:
"""For each configuration and room version, create the room in Python,
then build its events after the create event in Rust from the same
inputs: they come out identical, event IDs included."""
requester = create_requester(self.user_id, device_id="DEVICE")
for room_version in ROOM_VERSIONS:
for config in ROOM_CONFIGS:
with self.subTest(room_version=room_version, config=config):
self.handler._rust_room_creation = False
try:
room_id = self._create_room(room_version, config)
finally:
self.handler._rust_room_creation = True
events = self._room_events(room_id)
create, rest = events[0], events[1:]
# Stop at the first invite, which `create_event` builds.
rest = [
e
for e in rest
if not (
e.type == EventTypes.Member and e.membership == "invite"
)
]
# The clock does not move while the batch is built.
(ts,) = {e.origin_server_ts for e in rest}
with patch.object(
self.handler.clock,
"time_msec",
return_value=ts,
):
built = self.handler._build_room_events_in_rust(
requester,
KNOWN_ROOM_VERSIONS[room_version],
room_id,
[create.event_id],
create.depth + 1,
{(create.type, create.state_key): create.event_id},
[(e.type, e.state_key, e.content) for e in rest],
)
self.assertEqual(len(built), len(rest))
python_keys = {e.event_id: (e.type, e.state_key) for e in events}
rust_keys = {
e.event_id: (e.type, e.state_key) for e in [create] + built
}
for ours, theirs in zip(built, rest):
self.assertEqual(
_comparable(ours, rust_keys),
_comparable(theirs, python_keys),
)
self.assertEqual(ours.internal_metadata.device_id, "DEVICE")
def test_rooms_built_in_rust(self) -> None:
"""Rooms of every configuration are created with their events built in
Rust, and end up with the state Python would give them."""
for room_version in ROOM_VERSIONS:
for config in ROOM_CONFIGS:
with self.subTest(room_version=room_version, config=config):
before = _rust_rooms(self.hs, "rust")
room_id = self._create_room(room_version, config)
self.assertEqual(_rust_rooms(self.hs, "rust"), before + 1)
state = self.get_success(
self.hs.get_storage_controllers().state.get_current_state(
room_id
)
)
self.assertEqual(
state[(EventTypes.Member, self.user_id)].membership, "join"
)
self.assertIn((EventTypes.PowerLevels, ""), state)
def test_v1_rooms_fall_back(self) -> None:
before = _rust_rooms(self.hs, "fallback")
self._create_room("1", {})
self.assertEqual(_rust_rooms(self.hs, "fallback"), before + 1)
def test_aliases_in_initial_state_fall_back(self) -> None:
"""`m.room.aliases` needs the join persisted on its own."""
before = _rust_rooms(self.hs, "fallback")
self._create_room(
"10",
{
"initial_state": [
{"type": "m.room.aliases", "state_key": "test", "content": {}}
]
},
)
self.assertEqual(_rust_rooms(self.hs, "fallback"), before + 1)
def test_invalid_initial_state(self) -> None:
"""An event `validate_new` rejects fails the request as in Python."""
channel = self.make_request(
"POST",
"/createRoom",
{
"initial_state": [
{"type": "m.room.power_levels", "content": {"ban": "high"}}
]
},
access_token=self.tok,
)
self.assertEqual(channel.code, 400, channel.json_body)
self.assertEqual(channel.json_body["errcode"], "M_BAD_JSON")
def test_create_event_in_initial_state(self) -> None:
"""A second create event is refused by `create_event`, as in Python."""
before = _rust_rooms(self.hs, "fallback")
channel = self.make_request(
"POST",
"/createRoom",
{
"room_version": "10",
"initial_state": [
{"type": "m.room.create", "content": {"room_version": "10"}}
],
},
access_token=self.tok,
)
self.assertEqual(channel.code, 400, channel.json_body)
self.assertEqual(channel.json_body["errcode"], "M_INVALID_PARAM")
self.assertEqual(_rust_rooms(self.hs, "fallback"), before + 1)
def test_bad_input_is_not_a_rust_error(self) -> None:
"""Input Rust cannot build and Python rejects gets Python's error, even
in strict mode, and does not count as a Rust error."""
before = _rust_rooms(self.hs, "error")
channel = self.make_request(
"POST",
"/createRoom",
{
"initial_state": [
{
"type": "m.room.member",
"state_key": "@bob:test",
"content": {"displayname": "no membership"},
}
]
},
access_token=self.tok,
)
self.assertEqual(channel.code, 400, channel.json_body)
self.assertEqual(_rust_rooms(self.hs, "error"), before)
def test_rust_error_falls_back(self) -> None:
"""Outside strict mode, a Rust failure builds the room in Python."""
self.handler._rust_room_creation_strict = False
def fail(*args: Any, **kwargs: Any) -> Any:
raise ValueError("boom")
before = _rust_rooms(self.hs, "error")
with patch.object(self.handler, "_event_signer") as signer:
signer.build_room_events.side_effect = fail
room_id = self._create_room("10", {"name": "A room"})
self.assertEqual(_rust_rooms(self.hs, "error"), before + 1)
events = self._room_events(room_id)
self.assertEqual(
[e.type for e in events][:3],
[EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels],
)
@override_config({"experimental_features": {"rust_room_creation": "off"}})
def test_off(self) -> None:
if RUST_ROOM_CREATION_FOR_TESTS:
self.skipTest("SYNAPSE_TEST_RUST_ROOM_CREATION sets the mode")
before = _rust_rooms(self.hs, "rust")
self._create_room("10", {})
self.assertEqual(_rust_rooms(self.hs, "rust"), before)
def test_appservice_falls_back(self) -> None:
"""An appservice sender's events carry it in their context."""
appservice = Mock()
appservice.id = "an_appservice"
requester = create_requester(self.user_id, app_service=appservice)
self.assertTrue(
self.handler._rust_room_creation_unsupported(
requester, KNOWN_ROOM_VERSIONS["10"], False
)
)
self.assertFalse(
self.handler._rust_room_creation_unsupported(
create_requester(self.user_id), KNOWN_ROOM_VERSIONS["10"], False
)
)
+7 -3
View File
@@ -794,10 +794,14 @@ class RoomsCreateTestCase(RoomBase):
def _expected_create_room_txn_count(self) -> int:
"""The Python transactions a plain room creation runs. The state groups
are written in Rust on the native driver, outside them."""
are written in Rust on the native driver, outside them, and building the
events in Rust skips looking up the room version."""
count = 10
if self.hs.get_datastores().state._rust_state_group_writer is not None:
return 9
return 10
count -= 1
if self.hs.config.experimental.rust_room_creation == "on":
count -= 1
return count
def test_post_room_no_keys(self) -> None:
# POST with no config keys, expect new room id
+15
View File
@@ -97,6 +97,7 @@ from synapse.config.homeserver import HomeServerConfig
from synapse.events.auto_accept_invites import InviteAutoAccepter
from synapse.events.presence_router import load_legacy_presence_router
from synapse.handlers.auth import load_legacy_password_auth_providers
from synapse.handlers.room import RoomCreationHandler
from synapse.http.site import SynapseRequest
from synapse.logging.context import ContextResourceUsage, LoggingContext
from synapse.module_api.callbacks.spamchecker_callbacks import load_legacy_spam_checkers
@@ -138,6 +139,8 @@ from tests.utils import (
POSTGRES_USER,
RUST_PERSIST_FOR_TESTS,
RUST_PERSIST_STRICT_FOR_TESTS,
RUST_ROOM_CREATION_FOR_TESTS,
RUST_ROOM_CREATION_STRICT_FOR_TESTS,
RUST_SLIDING_SYNC_FOR_TESTS,
RUST_SLIDING_SYNC_STRICT_FOR_TESTS,
RUST_STATE_RES_FOR_TESTS,
@@ -1415,6 +1418,13 @@ def setup_test_homeserver(
Literal["off", "on"], RUST_SLIDING_SYNC_FOR_TESTS
)
if RUST_ROOM_CREATION_FOR_TESTS:
if RUST_ROOM_CREATION_FOR_TESTS not in ("off", "on"):
raise ValueError("SYNAPSE_TEST_RUST_ROOM_CREATION must be off or on")
config.experimental.rust_room_creation = cast(
Literal["off", "on"], RUST_ROOM_CREATION_FOR_TESTS
)
if USE_POSTGRES_FOR_TESTS:
test_db = "synapse_test_%s" % uuid.uuid4().hex
@@ -1643,6 +1653,11 @@ def setup_test_homeserver(
)
sync_handler._rust_sync_strict = RUST_SYNC_STRICT_FOR_TESTS
if RUST_ROOM_CREATION_FOR_TESTS:
RoomCreationHandler._rust_room_creation_strict = (
RUST_ROOM_CREATION_STRICT_FOR_TESTS
)
if RUST_SLIDING_SYNC_FOR_TESTS:
# Likewise for the Rust sliding sync handler.
sliding_sync_handler = hs.get_sliding_sync_handler()
+7
View File
@@ -78,6 +78,13 @@ RUST_SLIDING_SYNC_FOR_TESTS = os.environ.get("SYNAPSE_TEST_RUST_SLIDING_SYNC")
RUST_SLIDING_SYNC_STRICT_FOR_TESTS = (
os.environ.get("SYNAPSE_TEST_RUST_SLIDING_SYNC_STRICT", "1") != "0"
)
# As above, for `experimental_features.rust_room_creation` ("off" or "on").
RUST_ROOM_CREATION_FOR_TESTS = os.environ.get("SYNAPSE_TEST_RUST_ROOM_CREATION")
# A Rust error fails the test rather than falling back to building the room's
# events in Python. Set to 0 to fall back and count, as production does.
RUST_ROOM_CREATION_STRICT_FOR_TESTS = (
os.environ.get("SYNAPSE_TEST_RUST_ROOM_CREATION_STRICT", "1") != "0"
)
# At `rust_federation_sending: transactions`, check every transaction body Rust
# builds against the one Python would build, and fail the test if they differ.
RUST_FED_STRICT_FOR_TESTS = os.environ.get("SYNAPSE_TEST_RUST_FED_STRICT", "1") != "0"