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

Commit d478a36

Browse files
committed
Room Complexity Client Implementation (#5783)
2 parents 2765c80 + 865077f commit d478a36

File tree

8 files changed

+298
-14
lines changed

8 files changed

+298
-14
lines changed

changelog.d/5783.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Synapse can now be configured to not join remote rooms of a given "complexity" (currently, state events) over federation. This option can be used to prevent adverse performance on resource-constrained homeservers.

docs/sample_config.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,23 @@ listeners:
285285
# Used by phonehome stats to group together related servers.
286286
#server_context: context
287287

288+
# Resource-constrained Homeserver Settings
289+
#
290+
# If limit_remote_rooms.enabled is True, the room complexity will be
291+
# checked before a user joins a new remote room. If it is above
292+
# limit_remote_rooms.complexity, it will disallow joining or
293+
# instantly leave.
294+
#
295+
# limit_remote_rooms.complexity_error can be set to customise the text
296+
# displayed to the user when a room above the complexity threshold has
297+
# its join cancelled.
298+
#
299+
# Uncomment the below lines to enable:
300+
#limit_remote_rooms:
301+
# enabled: True
302+
# complexity: 1.0
303+
# complexity_error: "This room is too complex."
304+
288305
# Whether to require a user to be in the room to add an alias to it.
289306
# Defaults to 'true'.
290307
#

synapse/config/server.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import logging
1919
import os.path
2020

21+
import attr
2122
from netaddr import IPSet
2223

2324
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
@@ -38,6 +39,12 @@
3839

3940
DEFAULT_ROOM_VERSION = "4"
4041

42+
ROOM_COMPLEXITY_TOO_GREAT = (
43+
"Your homeserver is unable to join rooms this large or complex. "
44+
"Please speak to your server administrator, or upgrade your instance "
45+
"to join this room."
46+
)
47+
4148

4249
class ServerConfig(Config):
4350
def read_config(self, config, **kwargs):
@@ -377,6 +384,23 @@ def read_config(self, config, **kwargs):
377384

378385
self.gc_thresholds = read_gc_thresholds(config.get("gc_thresholds", None))
379386

387+
@attr.s
388+
class LimitRemoteRoomsConfig(object):
389+
enabled = attr.ib(
390+
validator=attr.validators.instance_of(bool), default=False
391+
)
392+
complexity = attr.ib(
393+
validator=attr.validators.instance_of((int, float)), default=1.0
394+
)
395+
complexity_error = attr.ib(
396+
validator=attr.validators.instance_of(str),
397+
default=ROOM_COMPLEXITY_TOO_GREAT,
398+
)
399+
400+
self.limit_remote_rooms = LimitRemoteRoomsConfig(
401+
**config.get("limit_remote_rooms", {})
402+
)
403+
380404
bind_port = config.get("bind_port")
381405
if bind_port:
382406
if config.get("no_tls", False):
@@ -754,6 +778,23 @@ def generate_config_section(
754778
# Used by phonehome stats to group together related servers.
755779
#server_context: context
756780
781+
# Resource-constrained Homeserver Settings
782+
#
783+
# If limit_remote_rooms.enabled is True, the room complexity will be
784+
# checked before a user joins a new remote room. If it is above
785+
# limit_remote_rooms.complexity, it will disallow joining or
786+
# instantly leave.
787+
#
788+
# limit_remote_rooms.complexity_error can be set to customise the text
789+
# displayed to the user when a room above the complexity threshold has
790+
# its join cancelled.
791+
#
792+
# Uncomment the below lines to enable:
793+
#limit_remote_rooms:
794+
# enabled: True
795+
# complexity: 1.0
796+
# complexity_error: "This room is too complex."
797+
757798
# Whether to require a user to be in the room to add an alias to it.
758799
# Defaults to 'true'.
759800
#

synapse/federation/federation_client.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,3 +993,39 @@ def forward_third_party_invite(self, destinations, room_id, event_dict):
993993
)
994994

995995
raise RuntimeError("Failed to send to any server.")
996+
997+
@defer.inlineCallbacks
998+
def get_room_complexity(self, destination, room_id):
999+
"""
1000+
Fetch the complexity of a remote room from another server.
1001+
1002+
Args:
1003+
destination (str): The remote server
1004+
room_id (str): The room ID to ask about.
1005+
1006+
Returns:
1007+
Deferred[dict] or Deferred[None]: Dict contains the complexity
1008+
metric versions, while None means we could not fetch the complexity.
1009+
"""
1010+
try:
1011+
complexity = yield self.transport_layer.get_room_complexity(
1012+
destination=destination, room_id=room_id
1013+
)
1014+
defer.returnValue(complexity)
1015+
except CodeMessageException as e:
1016+
# We didn't manage to get it -- probably a 404. We are okay if other
1017+
# servers don't give it to us.
1018+
logger.debug(
1019+
"Failed to fetch room complexity via %s for %s, got a %d",
1020+
destination,
1021+
room_id,
1022+
e.code,
1023+
)
1024+
except Exception:
1025+
logger.exception(
1026+
"Failed to fetch room complexity via %s for %s", destination, room_id
1027+
)
1028+
1029+
# If we don't manage to find it, return None. It's not an error if a
1030+
# server doesn't give it to us.
1031+
defer.returnValue(None)

synapse/federation/transport/client.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121
from twisted.internet import defer
2222

2323
from synapse.api.constants import Membership
24-
from synapse.api.urls import FEDERATION_V1_PREFIX, FEDERATION_V2_PREFIX
24+
from synapse.api.urls import (
25+
FEDERATION_UNSTABLE_PREFIX,
26+
FEDERATION_V1_PREFIX,
27+
FEDERATION_V2_PREFIX,
28+
)
2529
from synapse.logging.utils import log_function
2630

2731
logger = logging.getLogger(__name__)
@@ -935,6 +939,23 @@ def bulk_get_publicised_groups(self, destination, user_ids):
935939
destination=destination, path=path, data=content, ignore_backoff=True
936940
)
937941

942+
def get_room_complexity(self, destination, room_id):
943+
"""
944+
Args:
945+
destination (str): The remote server
946+
room_id (str): The room ID to ask about.
947+
"""
948+
path = _create_path(FEDERATION_UNSTABLE_PREFIX, "/rooms/%s/complexity", room_id)
949+
950+
return self.client.get_json(destination=destination, path=path)
951+
952+
953+
def _create_path(federation_prefix, path, *args):
954+
"""
955+
Ensures that all args are url encoded.
956+
"""
957+
return federation_prefix + path % tuple(urllib.parse.quote(arg, "") for arg in args)
958+
938959

939960
def _create_v1_path(path, *args):
940961
"""Creates a path against V1 federation API from the path template and
@@ -951,9 +972,7 @@ def _create_v1_path(path, *args):
951972
Returns:
952973
str
953974
"""
954-
return FEDERATION_V1_PREFIX + path % tuple(
955-
urllib.parse.quote(arg, "") for arg in args
956-
)
975+
return _create_path(FEDERATION_V1_PREFIX, path, *args)
957976

958977

959978
def _create_v2_path(path, *args):
@@ -971,6 +990,4 @@ def _create_v2_path(path, *args):
971990
Returns:
972991
str
973992
"""
974-
return FEDERATION_V2_PREFIX + path % tuple(
975-
urllib.parse.quote(arg, "") for arg in args
976-
)
993+
return _create_path(FEDERATION_V2_PREFIX, path, *args)

synapse/handlers/federation.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2808,3 +2808,28 @@ def user_joined_room(self, user, room_id):
28082808
)
28092809
else:
28102810
return user_joined_room(self.distributor, user, room_id)
2811+
2812+
@defer.inlineCallbacks
2813+
def get_room_complexity(self, remote_room_hosts, room_id):
2814+
"""
2815+
Fetch the complexity of a remote room over federation.
2816+
2817+
Args:
2818+
remote_room_hosts (list[str]): The remote servers to ask.
2819+
room_id (str): The room ID to ask about.
2820+
2821+
Returns:
2822+
Deferred[dict] or Deferred[None]: Dict contains the complexity
2823+
metric versions, while None means we could not fetch the complexity.
2824+
"""
2825+
2826+
for host in remote_room_hosts:
2827+
res = yield self.federation_client.get_room_complexity(host, room_id)
2828+
2829+
# We got a result, return it.
2830+
if res:
2831+
defer.returnValue(res)
2832+
2833+
# We fell off the bottom, couldn't get the complexity from anyone. Oh
2834+
# well.
2835+
defer.returnValue(None)

synapse/handlers/room_member.py

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222

2323
from twisted.internet import defer
2424

25-
import synapse.server
26-
import synapse.types
25+
from synapse import types
2726
from synapse.api.constants import EventTypes, Membership
2827
from synapse.api.ratelimiting import Ratelimiter
2928
from synapse.api.errors import (
@@ -592,7 +591,7 @@ def send_membership_event(
592591
), "Sender (%s) must be same as requester (%s)" % (sender, requester.user)
593592
assert self.hs.is_mine(sender), "Sender must be our own: %s" % (sender,)
594593
else:
595-
requester = synapse.types.create_requester(target_user)
594+
requester = types.create_requester(target_user)
596595

597596
prev_event = yield self.event_creation_handler.deduplicate_state_event(
598597
event, context
@@ -1011,21 +1010,73 @@ def __init__(self, hs):
10111010
self.distributor.declare("user_joined_room")
10121011
self.distributor.declare("user_left_room")
10131012

1013+
@defer.inlineCallbacks
1014+
def _is_remote_room_too_complex(self, room_id, remote_room_hosts):
1015+
"""
1016+
Check if complexity of a remote room is too great.
1017+
1018+
Args:
1019+
room_id (str)
1020+
remote_room_hosts (list[str])
1021+
1022+
Returns: bool of whether the complexity is too great, or None
1023+
if unable to be fetched
1024+
"""
1025+
max_complexity = self.hs.config.limit_remote_rooms.complexity
1026+
complexity = yield self.federation_handler.get_room_complexity(
1027+
remote_room_hosts, room_id
1028+
)
1029+
1030+
if complexity:
1031+
if complexity["v1"] > max_complexity:
1032+
return True
1033+
return False
1034+
return None
1035+
1036+
@defer.inlineCallbacks
1037+
def _is_local_room_too_complex(self, room_id):
1038+
"""
1039+
Check if the complexity of a local room is too great.
1040+
1041+
Args:
1042+
room_id (str)
1043+
1044+
Returns: bool
1045+
"""
1046+
max_complexity = self.hs.config.limit_remote_rooms.complexity
1047+
complexity = yield self.store.get_room_complexity(room_id)
1048+
1049+
if complexity["v1"] > max_complexity:
1050+
return True
1051+
1052+
return False
1053+
10141054
@defer.inlineCallbacks
10151055
def _remote_join(self, requester, remote_room_hosts, room_id, user, content):
10161056
"""Implements RoomMemberHandler._remote_join
10171057
"""
10181058
# filter ourselves out of remote_room_hosts: do_invite_join ignores it
10191059
# and if it is the only entry we'd like to return a 404 rather than a
10201060
# 500.
1021-
10221061
remote_room_hosts = [
10231062
host for host in remote_room_hosts if host != self.hs.hostname
10241063
]
10251064

10261065
if len(remote_room_hosts) == 0:
10271066
raise SynapseError(404, "No known servers")
10281067

1068+
if self.hs.config.limit_remote_rooms.enabled:
1069+
# Fetch the room complexity
1070+
too_complex = yield self._is_remote_room_too_complex(
1071+
room_id, remote_room_hosts
1072+
)
1073+
if too_complex is True:
1074+
raise SynapseError(
1075+
code=400,
1076+
msg=self.hs.config.limit_remote_rooms.complexity_error,
1077+
errcode=Codes.RESOURCE_LIMIT_EXCEEDED,
1078+
)
1079+
10291080
# We don't do an auth check if we are doing an invite
10301081
# join dance for now, since we're kinda implicitly checking
10311082
# that we are allowed to join when we decide whether or not we
@@ -1035,6 +1086,31 @@ def _remote_join(self, requester, remote_room_hosts, room_id, user, content):
10351086
)
10361087
yield self._user_joined_room(user, room_id)
10371088

1089+
# Check the room we just joined wasn't too large, if we didn't fetch the
1090+
# complexity of it before.
1091+
if self.hs.config.limit_remote_rooms.enabled:
1092+
if too_complex is False:
1093+
# We checked, and we're under the limit.
1094+
return
1095+
1096+
# Check again, but with the local state events
1097+
too_complex = yield self._is_local_room_too_complex(room_id)
1098+
1099+
if too_complex is False:
1100+
# We're under the limit.
1101+
return
1102+
1103+
# The room is too large. Leave.
1104+
requester = types.create_requester(user, None, False, None)
1105+
yield self.update_membership(
1106+
requester=requester, target=user, room_id=room_id, action="leave"
1107+
)
1108+
raise SynapseError(
1109+
code=400,
1110+
msg=self.hs.config.limit_remote_rooms.complexity_error,
1111+
errcode=Codes.RESOURCE_LIMIT_EXCEEDED,
1112+
)
1113+
10381114
@defer.inlineCallbacks
10391115
def _remote_reject_invite(self, requester, remote_room_hosts, room_id, target):
10401116
"""Implements RoomMemberHandler._remote_reject_invite

0 commit comments

Comments
 (0)