From f3688b5230e034f7dafcb6769290244f9a07e32d Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 9 Dec 2024 15:16:01 -0500 Subject: [PATCH 01/13] Define delayed event ratelimit category Apply ratelimiting on delayed event management separately from messages. --- demo/start.sh | 3 + .../conf/workers-shared-extra.yaml.j2 | 4 + .../configuration/config_documentation.md | 19 +++ synapse/config/ratelimiting.py | 6 + synapse/handlers/delayed_events.py | 11 +- synapse/server.py | 8 + tests/rest/client/test_delayed_events.py | 138 ++++++++++++++++++ tests/rest/client/test_rooms.py | 34 +++++ 8 files changed, 220 insertions(+), 3 deletions(-) diff --git a/demo/start.sh b/demo/start.sh index 06ec6f985f3..2eb59ebebb2 100755 --- a/demo/start.sh +++ b/demo/start.sh @@ -138,6 +138,9 @@ for port in 8080 8081 8082; do per_user: per_second: 1000 burst_count: 1000 + rc_delayed_event: + per_second: 1000 + burst_count: 1000 RC ) echo "${ratelimiting}" >> "$port.config" diff --git a/docker/complement/conf/workers-shared-extra.yaml.j2 b/docker/complement/conf/workers-shared-extra.yaml.j2 index c5228af72d0..c2018fc63b9 100644 --- a/docker/complement/conf/workers-shared-extra.yaml.j2 +++ b/docker/complement/conf/workers-shared-extra.yaml.j2 @@ -86,6 +86,10 @@ rc_invites: per_second: 1000 burst_count: 1000 +rc_delayed_event: + per_second: 9999 + burst_count: 9999 + federation_rr_transactions_per_room_per_second: 9999 allow_device_name_lookup_over_federation: true diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index 7a48d76bbb1..3c42c4bf43b 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -1866,6 +1866,25 @@ rc_federation: concurrent: 5 ``` --- +### `rc_delayed_event` + + +Ratelimiting settings for delayed event management. + +This is a ratelimiting option that ratelimits +attempts to restart, cancel, or view delayed events +based on the account the client is using. +It defaults to: `per_second: 10`, `burst_count: 100`. + +Attempts to create or send delayed events are ratelimited not by this setting, but by `rc_message`. + +Example configuration: +```yaml +rc_delayed_event: + per_second: 5 + burst_count: 50 +``` +--- ### `federation_rr_transactions_per_room_per_second` Sets outgoing federation transaction frequency for sending read-receipts, diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py index 3fa33f5373f..2594f6dfc60 100644 --- a/synapse/config/ratelimiting.py +++ b/synapse/config/ratelimiting.py @@ -228,3 +228,9 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None: config.get("remote_media_download_burst_count", "500M") ), ) + + self.rc_delayed_event = RatelimitSettings.parse( + config, + "rc_delayed_event", + defaults={"per_second": 10, "burst_count": 100}, + ) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index 3c88a96fd3f..efcd11beb56 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -58,6 +58,7 @@ def __init__(self, hs: "HomeServer"): self._config = hs.config self._clock = hs.get_clock() self._request_ratelimiter = hs.get_request_ratelimiter() + self._delayed_event_ratelimiter = hs.get_delayed_event_ratelimiter() self._event_creation_handler = hs.get_event_creation_handler() self._room_member_handler = hs.get_room_member_handler() @@ -227,6 +228,8 @@ async def add( Raises: SynapseError: if the delayed event fails validation checks. """ + # Use standard request limiter for scheduling new delayed events. + # TODO: Instead apply rateliming based on the scheduled send time. await self._request_ratelimiter.ratelimit(requester) self._event_creation_handler.validator.validate_builder( @@ -285,7 +288,7 @@ async def cancel(self, requester: Requester, delay_id: str) -> None: NotFoundError: if no matching delayed event could be found. """ assert self._is_master - await self._request_ratelimiter.ratelimit(requester) + await self._delayed_event_ratelimiter.ratelimit(requester) await self._initialized_from_db next_send_ts = await self._store.cancel_delayed_event( @@ -308,7 +311,7 @@ async def restart(self, requester: Requester, delay_id: str) -> None: NotFoundError: if no matching delayed event could be found. """ assert self._is_master - await self._request_ratelimiter.ratelimit(requester) + await self._delayed_event_ratelimiter.ratelimit(requester) await self._initialized_from_db next_send_ts = await self._store.restart_delayed_event( @@ -332,6 +335,8 @@ async def send(self, requester: Requester, delay_id: str) -> None: NotFoundError: if no matching delayed event could be found. """ assert self._is_master + # Use standard request limiter for sending delayed events on-demand, + # as an on-demand send is similar to sending a regular event. await self._request_ratelimiter.ratelimit(requester) await self._initialized_from_db @@ -415,7 +420,7 @@ def _schedule_next_at(self, next_send_ts: Timestamp) -> None: async def get_all_for_user(self, requester: Requester) -> List[JsonDict]: """Return all pending delayed events requested by the given user.""" - await self._request_ratelimiter.ratelimit(requester) + await self._delayed_event_ratelimiter.ratelimit(requester) return await self._store.get_all_delayed_events_for_user( requester.user.localpart ) diff --git a/synapse/server.py b/synapse/server.py index 462e15cc2ff..8a5fe7ef4f0 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -935,6 +935,14 @@ def get_request_ratelimiter(self) -> RequestRatelimiter: self.config.ratelimiting.rc_admin_redaction, ) + @cache_in_self + def get_delayed_event_ratelimiter(self) -> Ratelimiter: + return Ratelimiter( + store=self.get_datastores().main, + clock=self.get_clock(), + cfg=self.config.ratelimiting.rc_delayed_event, + ) + @cache_in_self def get_common_usage_metrics_manager(self) -> CommonUsageMetricsManager: """Usage metrics shared between phone home stats and the prometheus exporter.""" diff --git a/tests/rest/client/test_delayed_events.py b/tests/rest/client/test_delayed_events.py index 1793b38c4a8..8a55abbad73 100644 --- a/tests/rest/client/test_delayed_events.py +++ b/tests/rest/client/test_delayed_events.py @@ -109,6 +109,25 @@ def test_delayed_state_events_are_sent_on_timeout(self) -> None: ) self.assertEqual(setter_expected, content.get(setter_key), content) + @unittest.override_config({"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}}) + def test_get_delayed_events_ratelimit(self) -> None: + args = ("GET", PATH_PREFIX) + + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result) + + # Add the current user to the ratelimit overrides, allowing them no ratelimiting. + self.get_success( + self.hs.get_datastores().main.set_ratelimit_for_user(self.user_id, 0, 0) + ) + + # Test that the request isn't ratelimited anymore. + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + def test_update_delayed_event_without_id(self) -> None: channel = self.make_request( "POST", @@ -206,6 +225,44 @@ def test_cancel_delayed_state_event(self) -> None: expect_code=HTTPStatus.NOT_FOUND, ) + @unittest.override_config({"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}}) + def test_cancel_delayed_event_ratelimit(self) -> None: + delay_ids = [] + for i in range(2): + channel = self.make_request( + "POST", + _get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000), + {}, + ) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + delay_id = channel.json_body.get("delay_id") + self.assertIsNotNone(delay_id) + delay_ids.append(delay_id) + + channel = self.make_request( + "POST", + f"{PATH_PREFIX}/{delay_ids.pop(0)}", + {"action": "cancel"}, + ) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + + args = ( + "POST", + f"{PATH_PREFIX}/{delay_ids.pop(0)}", + {"action": "cancel"}, + ) + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result) + + # Add the current user to the ratelimit overrides, allowing them no ratelimiting. + self.get_success( + self.hs.get_datastores().main.set_ratelimit_for_user(self.user_id, 0, 0) + ) + + # Test that the request isn't ratelimited anymore. + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + def test_send_delayed_state_event(self) -> None: state_key = "to_send_on_request" @@ -250,6 +307,44 @@ def test_send_delayed_state_event(self) -> None: ) self.assertEqual(setter_expected, content.get(setter_key), content) + @unittest.override_config({"rc_message": {"per_second": 3, "burst_count": 4}}) + def test_send_delayed_event_ratelimit(self) -> None: + delay_ids = [] + for i in range(2): + channel = self.make_request( + "POST", + _get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000), + {}, + ) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + delay_id = channel.json_body.get("delay_id") + self.assertIsNotNone(delay_id) + delay_ids.append(delay_id) + + channel = self.make_request( + "POST", + f"{PATH_PREFIX}/{delay_ids.pop(0)}", + {"action": "send"}, + ) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + + args = ( + "POST", + f"{PATH_PREFIX}/{delay_ids.pop(0)}", + {"action": "send"}, + ) + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result) + + # Add the current user to the ratelimit overrides, allowing them no ratelimiting. + self.get_success( + self.hs.get_datastores().main.set_ratelimit_for_user(self.user_id, 0, 0) + ) + + # Test that the request isn't ratelimited anymore. + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + def test_restart_delayed_state_event(self) -> None: state_key = "to_send_on_restarted_timeout" @@ -309,6 +404,44 @@ def test_restart_delayed_state_event(self) -> None: ) self.assertEqual(setter_expected, content.get(setter_key), content) + @unittest.override_config({"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}}) + def test_restart_delayed_event_ratelimit(self) -> None: + delay_ids = [] + for i in range(2): + channel = self.make_request( + "POST", + _get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000), + {}, + ) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + delay_id = channel.json_body.get("delay_id") + self.assertIsNotNone(delay_id) + delay_ids.append(delay_id) + + channel = self.make_request( + "POST", + f"{PATH_PREFIX}/{delay_ids.pop(0)}", + {"action": "restart"}, + ) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + + args = ( + "POST", + f"{PATH_PREFIX}/{delay_ids.pop(0)}", + {"action": "restart"}, + ) + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result) + + # Add the current user to the ratelimit overrides, allowing them no ratelimiting. + self.get_success( + self.hs.get_datastores().main.set_ratelimit_for_user(self.user_id, 0, 0) + ) + + # Test that the request isn't ratelimited anymore. + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + def test_delayed_state_events_are_cancelled_by_more_recent_state(self) -> None: state_key = "to_be_cancelled" @@ -374,3 +507,8 @@ def _get_path_for_delayed_state( room_id: str, event_type: str, state_key: str, delay_ms: int ) -> str: return f"rooms/{room_id}/state/{event_type}/{state_key}?org.matrix.msc4140.delay={delay_ms}" + +def _get_path_for_delayed_send( + room_id: str, event_type: str, delay_ms: int +) -> str: + return f"rooms/{room_id}/send/{event_type}?org.matrix.msc4140.delay={delay_ms}" diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index 4cf1a3dc519..5f0d047b7fd 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -2382,6 +2382,40 @@ def test_send_delayed_state_event(self) -> None: ) self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + @unittest.override_config({ + "max_event_delay_duration": "24h", + "rc_message": {"per_second": 1, "burst_count": 2}, + }) + def test_add_delayed_event_ratelimit(self) -> None: + """Test that requests to schedule new delayed events are ratelimited by a RateLimiter, + which ratelimits them correctly, including by not limiting when the requester is + exempt from ratelimiting. + """ + + # Test that new delayed events are correctly ratelimited. + args = ( + "POST", + ( + "rooms/%s/send/m.room.message?org.matrix.msc4140.delay=2000" + % self.room_id + ).encode("ascii"), + {"body": "test", "msgtype": "m.text"}, + ) + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.TOO_MANY_REQUESTS, channel.code, channel.result) + + # Add the current user to the ratelimit overrides, allowing them no ratelimiting. + self.get_success( + self.hs.get_datastores().main.set_ratelimit_for_user(self.user_id, 0, 0) + ) + + # Test that the new delayed events aren't ratelimited anymore. + channel = self.make_request(*args) + self.assertEqual(HTTPStatus.OK, channel.code, channel.result) + + class RoomSearchTestCase(unittest.HomeserverTestCase): servlets = [ From 05318492de49e6283106f47409d7ed8e5620675f Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 9 Dec 2024 15:18:30 -0500 Subject: [PATCH 02/13] Add changelog --- changelog.d/18019.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/18019.feature diff --git a/changelog.d/18019.feature b/changelog.d/18019.feature new file mode 100644 index 00000000000..74e22df74a7 --- /dev/null +++ b/changelog.d/18019.feature @@ -0,0 +1 @@ +Define ratelimit configuration for delayed event management. From 87f57cdf4bb2c96fac437a27ff4f74c8b80de776 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 7 Feb 2025 14:09:06 -0500 Subject: [PATCH 03/13] Link comment to GH issue it relates to Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- synapse/handlers/delayed_events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index efcd11beb56..e87e007db3e 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -230,6 +230,7 @@ async def add( """ # Use standard request limiter for scheduling new delayed events. # TODO: Instead apply rateliming based on the scheduled send time. + # See https://github.com/element-hq/synapse/issues/18021 await self._request_ratelimiter.ratelimit(requester) self._event_creation_handler.validator.validate_builder( From b1dd54840129596e6a5bedf2b4b2d1645c2cadf2 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 7 Feb 2025 14:17:32 -0500 Subject: [PATCH 04/13] Use _ for unreferenced loop variables in test --- tests/rest/client/test_delayed_events.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rest/client/test_delayed_events.py b/tests/rest/client/test_delayed_events.py index 8a55abbad73..1602688f7a9 100644 --- a/tests/rest/client/test_delayed_events.py +++ b/tests/rest/client/test_delayed_events.py @@ -228,7 +228,7 @@ def test_cancel_delayed_state_event(self) -> None: @unittest.override_config({"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}}) def test_cancel_delayed_event_ratelimit(self) -> None: delay_ids = [] - for i in range(2): + for _ in range(2): channel = self.make_request( "POST", _get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000), @@ -310,7 +310,7 @@ def test_send_delayed_state_event(self) -> None: @unittest.override_config({"rc_message": {"per_second": 3, "burst_count": 4}}) def test_send_delayed_event_ratelimit(self) -> None: delay_ids = [] - for i in range(2): + for _ in range(2): channel = self.make_request( "POST", _get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000), @@ -407,7 +407,7 @@ def test_restart_delayed_state_event(self) -> None: @unittest.override_config({"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}}) def test_restart_delayed_event_ratelimit(self) -> None: delay_ids = [] - for i in range(2): + for _ in range(2): channel = self.make_request( "POST", _get_path_for_delayed_send(self.room_id, _EVENT_TYPE, 100000), From b2b1e228fd37336e32288b76819d786e56872905 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 7 Feb 2025 15:24:32 -0500 Subject: [PATCH 05/13] Document the purpose of a high rc_delayed_event --- docs/usage/configuration/config_documentation.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index 3c42c4bf43b..e0c3ec42074 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -1878,6 +1878,11 @@ It defaults to: `per_second: 10`, `burst_count: 100`. Attempts to create or send delayed events are ratelimited not by this setting, but by `rc_message`. +Setting this to a high value allows clients to make delayed event management requests often +(such as repeatedly restarting a delayed event with a short timeout, +or restarting several different delayed events all at once) +without the risk of being ratelimited. + Example configuration: ```yaml rc_delayed_event: From 435c32cb625faad97776862803cb66d93806f2ac Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 7 Feb 2025 15:26:17 -0500 Subject: [PATCH 06/13] Move creation of delayed event ratelimiter --- synapse/handlers/delayed_events.py | 12 ++++++++++-- synapse/server.py | 8 -------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index e87e007db3e..863986d5996 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -57,11 +57,19 @@ def __init__(self, hs: "HomeServer"): self._storage_controllers = hs.get_storage_controllers() self._config = hs.config self._clock = hs.get_clock() - self._request_ratelimiter = hs.get_request_ratelimiter() - self._delayed_event_ratelimiter = hs.get_delayed_event_ratelimiter() self._event_creation_handler = hs.get_event_creation_handler() self._room_member_handler = hs.get_room_member_handler() + self._request_ratelimiter = hs.get_request_ratelimiter() + + # Ratelimiter for management of existing delayed events, + # keyed by the sending user ID. + self._delayed_event_ratelimiter = Ratelimiter( + store=self.get_datastores().main, + clock=self.get_clock(), + cfg=self.config.ratelimiting.rc_delayed_event, + ) + self._next_delayed_event_call: Optional[IDelayedCall] = None # The current position in the current_state_delta stream diff --git a/synapse/server.py b/synapse/server.py index 8a5fe7ef4f0..462e15cc2ff 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -935,14 +935,6 @@ def get_request_ratelimiter(self) -> RequestRatelimiter: self.config.ratelimiting.rc_admin_redaction, ) - @cache_in_self - def get_delayed_event_ratelimiter(self) -> Ratelimiter: - return Ratelimiter( - store=self.get_datastores().main, - clock=self.get_clock(), - cfg=self.config.ratelimiting.rc_delayed_event, - ) - @cache_in_self def get_common_usage_metrics_manager(self) -> CommonUsageMetricsManager: """Usage metrics shared between phone home stats and the prometheus exporter.""" From 5b2ae488048196a4166b02ad1c4cda6267aef500 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 7 Feb 2025 15:28:17 -0500 Subject: [PATCH 07/13] Key delayed event ratelimit per device ID Also change the default values appropriately --- docs/usage/configuration/config_documentation.md | 8 ++++---- synapse/config/ratelimiting.py | 2 +- synapse/handlers/delayed_events.py | 14 ++++++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index e0c3ec42074..29a12c5edb8 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -1873,8 +1873,8 @@ Ratelimiting settings for delayed event management. This is a ratelimiting option that ratelimits attempts to restart, cancel, or view delayed events -based on the account the client is using. -It defaults to: `per_second: 10`, `burst_count: 100`. +based on the sending client's account and device ID. +It defaults to: `per_second: 1`, `burst_count: 5`. Attempts to create or send delayed events are ratelimited not by this setting, but by `rc_message`. @@ -1886,8 +1886,8 @@ without the risk of being ratelimited. Example configuration: ```yaml rc_delayed_event: - per_second: 5 - burst_count: 50 + per_second: 2 + burst_count: 20 ``` --- ### `federation_rr_transactions_per_room_per_second` diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py index 2594f6dfc60..6965e0e2870 100644 --- a/synapse/config/ratelimiting.py +++ b/synapse/config/ratelimiting.py @@ -232,5 +232,5 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None: self.rc_delayed_event = RatelimitSettings.parse( config, "rc_delayed_event", - defaults={"per_second": 10, "burst_count": 100}, + defaults={"per_second": 1, "burst_count": 5}, ) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index 863986d5996..eb3ea934bc0 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -63,7 +63,7 @@ def __init__(self, hs: "HomeServer"): self._request_ratelimiter = hs.get_request_ratelimiter() # Ratelimiter for management of existing delayed events, - # keyed by the sending user ID. + # keyed by the sending user ID & device ID. self._delayed_event_ratelimiter = Ratelimiter( store=self.get_datastores().main, clock=self.get_clock(), @@ -297,7 +297,9 @@ async def cancel(self, requester: Requester, delay_id: str) -> None: NotFoundError: if no matching delayed event could be found. """ assert self._is_master - await self._delayed_event_ratelimiter.ratelimit(requester) + await self._delayed_event_ratelimiter.ratelimit( + requester, (requester.user.to_string(), requester.device_id), + ) await self._initialized_from_db next_send_ts = await self._store.cancel_delayed_event( @@ -320,7 +322,9 @@ async def restart(self, requester: Requester, delay_id: str) -> None: NotFoundError: if no matching delayed event could be found. """ assert self._is_master - await self._delayed_event_ratelimiter.ratelimit(requester) + await self._delayed_event_ratelimiter.ratelimit( + requester, (requester.user.to_string(), requester.device_id), + ) await self._initialized_from_db next_send_ts = await self._store.restart_delayed_event( @@ -429,7 +433,9 @@ def _schedule_next_at(self, next_send_ts: Timestamp) -> None: async def get_all_for_user(self, requester: Requester) -> List[JsonDict]: """Return all pending delayed events requested by the given user.""" - await self._delayed_event_ratelimiter.ratelimit(requester) + await self._delayed_event_ratelimiter.ratelimit( + requester, (requester.user.to_string(), requester.device_id), + ) return await self._store.get_all_delayed_events_for_user( requester.user.localpart ) From 54fac20554982c3b116ddc7202e083fbb14a73f3 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 7 Feb 2025 15:47:35 -0500 Subject: [PATCH 08/13] Add missing import --- synapse/handlers/delayed_events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index eb3ea934bc0..c415cec4314 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -19,6 +19,7 @@ from synapse.api.constants import EventTypes from synapse.api.errors import ShadowBanError +from synapse.api.ratelimiting import Ratelimiter from synapse.config.workers import MAIN_PROCESS_INSTANCE_NAME from synapse.logging.opentracing import set_tag from synapse.metrics import event_processing_positions From 60a57960fa4a4c7a3b4435ce18e1ee0de6c8ddb4 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 7 Feb 2025 15:52:16 -0500 Subject: [PATCH 09/13] Lint modified files --- synapse/handlers/delayed_events.py | 9 ++++++--- tests/rest/client/test_delayed_events.py | 17 +++++++++++------ tests/rest/client/test_rooms.py | 11 ++++++----- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index c415cec4314..108742a920d 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -299,7 +299,8 @@ async def cancel(self, requester: Requester, delay_id: str) -> None: """ assert self._is_master await self._delayed_event_ratelimiter.ratelimit( - requester, (requester.user.to_string(), requester.device_id), + requester, + (requester.user.to_string(), requester.device_id), ) await self._initialized_from_db @@ -324,7 +325,8 @@ async def restart(self, requester: Requester, delay_id: str) -> None: """ assert self._is_master await self._delayed_event_ratelimiter.ratelimit( - requester, (requester.user.to_string(), requester.device_id), + requester, + (requester.user.to_string(), requester.device_id), ) await self._initialized_from_db @@ -435,7 +437,8 @@ def _schedule_next_at(self, next_send_ts: Timestamp) -> None: async def get_all_for_user(self, requester: Requester) -> List[JsonDict]: """Return all pending delayed events requested by the given user.""" await self._delayed_event_ratelimiter.ratelimit( - requester, (requester.user.to_string(), requester.device_id), + requester, + (requester.user.to_string(), requester.device_id), ) return await self._store.get_all_delayed_events_for_user( requester.user.localpart diff --git a/tests/rest/client/test_delayed_events.py b/tests/rest/client/test_delayed_events.py index 1602688f7a9..56140d88fb2 100644 --- a/tests/rest/client/test_delayed_events.py +++ b/tests/rest/client/test_delayed_events.py @@ -109,7 +109,9 @@ def test_delayed_state_events_are_sent_on_timeout(self) -> None: ) self.assertEqual(setter_expected, content.get(setter_key), content) - @unittest.override_config({"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}}) + @unittest.override_config( + {"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}} + ) def test_get_delayed_events_ratelimit(self) -> None: args = ("GET", PATH_PREFIX) @@ -225,7 +227,9 @@ def test_cancel_delayed_state_event(self) -> None: expect_code=HTTPStatus.NOT_FOUND, ) - @unittest.override_config({"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}}) + @unittest.override_config( + {"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}} + ) def test_cancel_delayed_event_ratelimit(self) -> None: delay_ids = [] for _ in range(2): @@ -404,7 +408,9 @@ def test_restart_delayed_state_event(self) -> None: ) self.assertEqual(setter_expected, content.get(setter_key), content) - @unittest.override_config({"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}}) + @unittest.override_config( + {"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}} + ) def test_restart_delayed_event_ratelimit(self) -> None: delay_ids = [] for _ in range(2): @@ -508,7 +514,6 @@ def _get_path_for_delayed_state( ) -> str: return f"rooms/{room_id}/state/{event_type}/{state_key}?org.matrix.msc4140.delay={delay_ms}" -def _get_path_for_delayed_send( - room_id: str, event_type: str, delay_ms: int -) -> str: + +def _get_path_for_delayed_send(room_id: str, event_type: str, delay_ms: int) -> str: return f"rooms/{room_id}/send/{event_type}?org.matrix.msc4140.delay={delay_ms}" diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py index 5f0d047b7fd..8a50e9b3cb2 100644 --- a/tests/rest/client/test_rooms.py +++ b/tests/rest/client/test_rooms.py @@ -2382,10 +2382,12 @@ def test_send_delayed_state_event(self) -> None: ) self.assertEqual(HTTPStatus.OK, channel.code, channel.result) - @unittest.override_config({ - "max_event_delay_duration": "24h", - "rc_message": {"per_second": 1, "burst_count": 2}, - }) + @unittest.override_config( + { + "max_event_delay_duration": "24h", + "rc_message": {"per_second": 1, "burst_count": 2}, + } + ) def test_add_delayed_event_ratelimit(self) -> None: """Test that requests to schedule new delayed events are ratelimited by a RateLimiter, which ratelimits them correctly, including by not limiting when the requester is @@ -2416,7 +2418,6 @@ def test_add_delayed_event_ratelimit(self) -> None: self.assertEqual(HTTPStatus.OK, channel.code, channel.result) - class RoomSearchTestCase(unittest.HomeserverTestCase): servlets = [ synapse.rest.admin.register_servlets_for_client_rest_resource, From 02b669b683af0d7dc42880bca90a06a7ae79f77d Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 7 Feb 2025 16:02:52 -0500 Subject: [PATCH 10/13] Fix Ratelimiter construction --- synapse/handlers/delayed_events.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index 108742a920d..510ba65dccf 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -66,9 +66,9 @@ def __init__(self, hs: "HomeServer"): # Ratelimiter for management of existing delayed events, # keyed by the sending user ID & device ID. self._delayed_event_ratelimiter = Ratelimiter( - store=self.get_datastores().main, - clock=self.get_clock(), - cfg=self.config.ratelimiting.rc_delayed_event, + store=self._store, + clock=self._clock, + cfg=self._config.ratelimiting.rc_delayed_event, ) self._next_delayed_event_call: Optional[IDelayedCall] = None From bb961995e891e8dfca5a5ca90c478b2e5f2fb475 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Fri, 7 Feb 2025 16:18:51 -0500 Subject: [PATCH 11/13] Fix test config --- tests/rest/client/test_delayed_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rest/client/test_delayed_events.py b/tests/rest/client/test_delayed_events.py index 56140d88fb2..303a0e27175 100644 --- a/tests/rest/client/test_delayed_events.py +++ b/tests/rest/client/test_delayed_events.py @@ -311,7 +311,7 @@ def test_send_delayed_state_event(self) -> None: ) self.assertEqual(setter_expected, content.get(setter_key), content) - @unittest.override_config({"rc_message": {"per_second": 3, "burst_count": 4}}) + @unittest.override_config({"rc_message": {"per_second": 3.5, "burst_count": 4}}) def test_send_delayed_event_ratelimit(self) -> None: delay_ids = [] for _ in range(2): From 34427f4968362fe5eca57e49f20c6b827b04a86c Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:59:45 +0700 Subject: [PATCH 12/13] fix typo in comment --- synapse/handlers/delayed_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index 510ba65dccf..49f0ec9238c 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -238,7 +238,7 @@ async def add( SynapseError: if the delayed event fails validation checks. """ # Use standard request limiter for scheduling new delayed events. - # TODO: Instead apply rateliming based on the scheduled send time. + # TODO: Instead apply ratelimiting based on the scheduled send time. # See https://github.com/element-hq/synapse/issues/18021 await self._request_ratelimiter.ratelimit(requester) From b014c078cbeadb40324a830d23ceb48ec0536f14 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 10 Feb 2025 08:32:59 -0500 Subject: [PATCH 13/13] Rename ratelimit config key to clarify its purpose This rate limit is for the _management_ of delayed events, not the sending of the events themselves (which will be managed by a different ratelimit setting to be added later). --- demo/start.sh | 2 +- docker/complement/conf/workers-shared-extra.yaml.j2 | 2 +- docs/usage/configuration/config_documentation.md | 4 ++-- synapse/config/ratelimiting.py | 4 ++-- synapse/handlers/delayed_events.py | 10 +++++----- tests/rest/client/test_delayed_events.py | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/demo/start.sh b/demo/start.sh index 2eb59ebebb2..9c81e288911 100755 --- a/demo/start.sh +++ b/demo/start.sh @@ -138,7 +138,7 @@ for port in 8080 8081 8082; do per_user: per_second: 1000 burst_count: 1000 - rc_delayed_event: + rc_delayed_event_mgmt: per_second: 1000 burst_count: 1000 RC diff --git a/docker/complement/conf/workers-shared-extra.yaml.j2 b/docker/complement/conf/workers-shared-extra.yaml.j2 index c8551b2bcee..58bbff69feb 100644 --- a/docker/complement/conf/workers-shared-extra.yaml.j2 +++ b/docker/complement/conf/workers-shared-extra.yaml.j2 @@ -94,7 +94,7 @@ rc_presence: per_second: 9999 burst_count: 9999 -rc_delayed_event: +rc_delayed_event_mgmt: per_second: 9999 burst_count: 9999 diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md index e1a7d1c0435..5f7dcf4b09c 100644 --- a/docs/usage/configuration/config_documentation.md +++ b/docs/usage/configuration/config_documentation.md @@ -1889,7 +1889,7 @@ rc_presence: burst_count: 0.5 ``` --- -### `rc_delayed_event` +### `rc_delayed_event_mgmt` Ratelimiting settings for delayed event management. @@ -1907,7 +1907,7 @@ without the risk of being ratelimited. Example configuration: ```yaml -rc_delayed_event: +rc_delayed_event_mgmt: per_second: 2 burst_count: 20 ``` diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py index ff570830ce7..eb1dc2dacbf 100644 --- a/synapse/config/ratelimiting.py +++ b/synapse/config/ratelimiting.py @@ -235,8 +235,8 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None: defaults={"per_second": 0.1, "burst_count": 1}, ) - self.rc_delayed_event = RatelimitSettings.parse( + self.rc_delayed_event_mgmt = RatelimitSettings.parse( config, - "rc_delayed_event", + "rc_delayed_event_mgmt", defaults={"per_second": 1, "burst_count": 5}, ) diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index 49f0ec9238c..b3f40809a14 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -65,10 +65,10 @@ def __init__(self, hs: "HomeServer"): # Ratelimiter for management of existing delayed events, # keyed by the sending user ID & device ID. - self._delayed_event_ratelimiter = Ratelimiter( + self._delayed_event_mgmt_ratelimiter = Ratelimiter( store=self._store, clock=self._clock, - cfg=self._config.ratelimiting.rc_delayed_event, + cfg=self._config.ratelimiting.rc_delayed_event_mgmt, ) self._next_delayed_event_call: Optional[IDelayedCall] = None @@ -298,7 +298,7 @@ async def cancel(self, requester: Requester, delay_id: str) -> None: NotFoundError: if no matching delayed event could be found. """ assert self._is_master - await self._delayed_event_ratelimiter.ratelimit( + await self._delayed_event_mgmt_ratelimiter.ratelimit( requester, (requester.user.to_string(), requester.device_id), ) @@ -324,7 +324,7 @@ async def restart(self, requester: Requester, delay_id: str) -> None: NotFoundError: if no matching delayed event could be found. """ assert self._is_master - await self._delayed_event_ratelimiter.ratelimit( + await self._delayed_event_mgmt_ratelimiter.ratelimit( requester, (requester.user.to_string(), requester.device_id), ) @@ -436,7 +436,7 @@ def _schedule_next_at(self, next_send_ts: Timestamp) -> None: async def get_all_for_user(self, requester: Requester) -> List[JsonDict]: """Return all pending delayed events requested by the given user.""" - await self._delayed_event_ratelimiter.ratelimit( + await self._delayed_event_mgmt_ratelimiter.ratelimit( requester, (requester.user.to_string(), requester.device_id), ) diff --git a/tests/rest/client/test_delayed_events.py b/tests/rest/client/test_delayed_events.py index 303a0e27175..2c938390c86 100644 --- a/tests/rest/client/test_delayed_events.py +++ b/tests/rest/client/test_delayed_events.py @@ -110,7 +110,7 @@ def test_delayed_state_events_are_sent_on_timeout(self) -> None: self.assertEqual(setter_expected, content.get(setter_key), content) @unittest.override_config( - {"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}} + {"rc_delayed_event_mgmt": {"per_second": 0.5, "burst_count": 1}} ) def test_get_delayed_events_ratelimit(self) -> None: args = ("GET", PATH_PREFIX) @@ -228,7 +228,7 @@ def test_cancel_delayed_state_event(self) -> None: ) @unittest.override_config( - {"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}} + {"rc_delayed_event_mgmt": {"per_second": 0.5, "burst_count": 1}} ) def test_cancel_delayed_event_ratelimit(self) -> None: delay_ids = [] @@ -409,7 +409,7 @@ def test_restart_delayed_state_event(self) -> None: self.assertEqual(setter_expected, content.get(setter_key), content) @unittest.override_config( - {"rc_delayed_event": {"per_second": 0.5, "burst_count": 1}} + {"rc_delayed_event_mgmt": {"per_second": 0.5, "burst_count": 1}} ) def test_restart_delayed_event_ratelimit(self) -> None: delay_ids = []