mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-12-27 01:29:24 -05:00
f4e6495b5d
While working on https://github.com/matrix-org/synapse/issues/5665 I found myself digging into the `Ratelimiter` class and seeing that it was both: * Rather undocumented, and * causing a *lot* of config checks This PR attempts to refactor and comment the `Ratelimiter` class, as well as encourage config file accesses to only be done at instantiation. Best to be reviewed commit-by-commit.
177 lines
7.0 KiB
Python
177 lines
7.0 KiB
Python
# Copyright 2014-2016 OpenMarket Ltd
|
|
# Copyright 2020 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.
|
|
|
|
from collections import OrderedDict
|
|
from typing import Any, Optional, Tuple
|
|
|
|
from synapse.api.errors import LimitExceededError
|
|
from synapse.util import Clock
|
|
|
|
|
|
class Ratelimiter(object):
|
|
"""
|
|
Ratelimit actions marked by arbitrary keys.
|
|
|
|
Args:
|
|
clock: A homeserver clock, for retrieving the current time
|
|
rate_hz: The long term number of actions that can be performed in a second.
|
|
burst_count: How many actions that can be performed before being limited.
|
|
"""
|
|
|
|
def __init__(self, clock: Clock, rate_hz: float, burst_count: int):
|
|
self.clock = clock
|
|
self.rate_hz = rate_hz
|
|
self.burst_count = burst_count
|
|
|
|
# A ordered dictionary keeping track of actions, when they were last
|
|
# performed and how often. Each entry is a mapping from a key of arbitrary type
|
|
# to a tuple representing:
|
|
# * How many times an action has occurred since a point in time
|
|
# * The point in time
|
|
# * The rate_hz of this particular entry. This can vary per request
|
|
self.actions = OrderedDict() # type: OrderedDict[Any, Tuple[float, int, float]]
|
|
|
|
def can_do_action(
|
|
self,
|
|
key: Any,
|
|
rate_hz: Optional[float] = None,
|
|
burst_count: Optional[int] = None,
|
|
update: bool = True,
|
|
_time_now_s: Optional[int] = None,
|
|
) -> Tuple[bool, float]:
|
|
"""Can the entity (e.g. user or IP address) perform the action?
|
|
|
|
Args:
|
|
key: The key we should use when rate limiting. Can be a user ID
|
|
(when sending events), an IP address, etc.
|
|
rate_hz: The long term number of actions that can be performed in a second.
|
|
Overrides the value set during instantiation if set.
|
|
burst_count: How many actions that can be performed before being limited.
|
|
Overrides the value set during instantiation if set.
|
|
update: Whether to count this check as performing the action
|
|
_time_now_s: The current time. Optional, defaults to the current time according
|
|
to self.clock. Only used by tests.
|
|
|
|
Returns:
|
|
A tuple containing:
|
|
* A bool indicating if they can perform the action now
|
|
* The reactor timestamp for when the action can be performed next.
|
|
-1 if rate_hz is less than or equal to zero
|
|
"""
|
|
# Override default values if set
|
|
time_now_s = _time_now_s if _time_now_s is not None else self.clock.time()
|
|
rate_hz = rate_hz if rate_hz is not None else self.rate_hz
|
|
burst_count = burst_count if burst_count is not None else self.burst_count
|
|
|
|
# Remove any expired entries
|
|
self._prune_message_counts(time_now_s)
|
|
|
|
# Check if there is an existing count entry for this key
|
|
action_count, time_start, _ = self.actions.get(key, (0.0, time_now_s, 0.0))
|
|
|
|
# Check whether performing another action is allowed
|
|
time_delta = time_now_s - time_start
|
|
performed_count = action_count - time_delta * rate_hz
|
|
if performed_count < 0:
|
|
# Allow, reset back to count 1
|
|
allowed = True
|
|
time_start = time_now_s
|
|
action_count = 1.0
|
|
elif performed_count > burst_count - 1.0:
|
|
# Deny, we have exceeded our burst count
|
|
allowed = False
|
|
else:
|
|
# We haven't reached our limit yet
|
|
allowed = True
|
|
action_count += 1.0
|
|
|
|
if update:
|
|
self.actions[key] = (action_count, time_start, rate_hz)
|
|
|
|
if rate_hz > 0:
|
|
# Find out when the count of existing actions expires
|
|
time_allowed = time_start + (action_count - burst_count + 1) / rate_hz
|
|
|
|
# Don't give back a time in the past
|
|
if time_allowed < time_now_s:
|
|
time_allowed = time_now_s
|
|
|
|
else:
|
|
# XXX: Why is this -1? This seems to only be used in
|
|
# self.ratelimit. I guess so that clients get a time in the past and don't
|
|
# feel afraid to try again immediately
|
|
time_allowed = -1
|
|
|
|
return allowed, time_allowed
|
|
|
|
def _prune_message_counts(self, time_now_s: int):
|
|
"""Remove message count entries that have not exceeded their defined
|
|
rate_hz limit
|
|
|
|
Args:
|
|
time_now_s: The current time
|
|
"""
|
|
# We create a copy of the key list here as the dictionary is modified during
|
|
# the loop
|
|
for key in list(self.actions.keys()):
|
|
action_count, time_start, rate_hz = self.actions[key]
|
|
|
|
# Rate limit = "seconds since we started limiting this action" * rate_hz
|
|
# If this limit has not been exceeded, wipe our record of this action
|
|
time_delta = time_now_s - time_start
|
|
if action_count - time_delta * rate_hz > 0:
|
|
continue
|
|
else:
|
|
del self.actions[key]
|
|
|
|
def ratelimit(
|
|
self,
|
|
key: Any,
|
|
rate_hz: Optional[float] = None,
|
|
burst_count: Optional[int] = None,
|
|
update: bool = True,
|
|
_time_now_s: Optional[int] = None,
|
|
):
|
|
"""Checks if an action can be performed. If not, raises a LimitExceededError
|
|
|
|
Args:
|
|
key: An arbitrary key used to classify an action
|
|
rate_hz: The long term number of actions that can be performed in a second.
|
|
Overrides the value set during instantiation if set.
|
|
burst_count: How many actions that can be performed before being limited.
|
|
Overrides the value set during instantiation if set.
|
|
update: Whether to count this check as performing the action
|
|
_time_now_s: The current time. Optional, defaults to the current time according
|
|
to self.clock. Only used by tests.
|
|
|
|
Raises:
|
|
LimitExceededError: If an action could not be performed, along with the time in
|
|
milliseconds until the action can be performed again
|
|
"""
|
|
time_now_s = _time_now_s if _time_now_s is not None else self.clock.time()
|
|
|
|
allowed, time_allowed = self.can_do_action(
|
|
key,
|
|
rate_hz=rate_hz,
|
|
burst_count=burst_count,
|
|
update=update,
|
|
_time_now_s=time_now_s,
|
|
)
|
|
|
|
if not allowed:
|
|
raise LimitExceededError(
|
|
retry_after_ms=int(1000 * (time_allowed - time_now_s))
|
|
)
|