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

Commit

Permalink
Make rate_hz and burst_count overridable per-request
Browse files Browse the repository at this point in the history
  • Loading branch information
anoadragon453 committed May 29, 2020
1 parent c322ba0 commit f6203a6
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 32 deletions.
66 changes: 47 additions & 19 deletions synapse/api/ratelimiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

from collections import OrderedDict
from typing import Any, Tuple
from typing import Any, Optional, Tuple

from synapse.api.errors import LimitExceededError

Expand All @@ -23,10 +23,8 @@ class Ratelimiter(object):
Ratelimit actions marked by arbitrary keys.
Args:
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.
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, rate_hz: float, burst_count: int):
Expand All @@ -40,7 +38,12 @@ def __init__(self, rate_hz: float, burst_count: int):
self.burst_count = burst_count

def can_do_action(
self, key: Any, time_now_s: int, update: bool = True,
self,
key: Any,
time_now_s: int,
update: bool = True,
rate_hz: Optional[float] = None,
burst_count: Optional[int] = None,
) -> Tuple[bool, float]:
"""Can the entity (e.g. user or IP address) perform the action?
Expand All @@ -49,27 +52,36 @@ def can_do_action(
(when sending events), an IP address, etc.
time_now_s: The time now
update: Whether to count this check as performing the 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.
Returns:
A tuple containing:
* A bool indicating if they can perform the action now
* The time in seconds of when it can next be performed.
-1 if a rate_hz has not been defined for this Ratelimiter
"""
# Override default values if set
rate_hz = rate_hz or self.rate_hz
burst_count = burst_count or self.burst_count

# Remove any expired entries
self._prune_message_counts(time_now_s)
self._prune_message_counts(time_now_s, rate_hz)

# Check if there is an existing count entry for this key
action_count, time_start, = self.actions.get(key, (0.0, time_now_s))

# Check whether performing another action is allowed
time_delta = time_now_s - time_start
performed_count = action_count - time_delta * self.rate_hz
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 > self.burst_count - 1.0:
elif performed_count > burst_count - 1.0:
# Deny, we have exceeded our burst count
allowed = False
else:
Expand All @@ -82,9 +94,7 @@ def can_do_action(

# Figure out the time when an action can be performed again
if self.rate_hz > 0:
time_allowed = (
time_start + (action_count - self.burst_count + 1) / self.rate_hz
)
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:
Expand All @@ -95,39 +105,57 @@ def can_do_action(

return allowed, time_allowed

def _prune_message_counts(self, time_now_s: int):
"""Remove message count entries that are older than
def _prune_message_counts(self, time_now_s: int, rate_hz: float):
"""Remove message count entries that have not exceeded their defined
rate_hz limit
Args:
time_now_s: The current time
rate_hz: The long term number of actions that can be performed in a second.
"""
# 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 = 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 * self.rate_hz > 0:
# XXX: Should this be a continue?
break
if action_count - time_delta * rate_hz > 0:
continue
else:
del self.actions[key]

def ratelimit(
self, key: Any, time_now_s: int, update: bool = True,
self,
key: Any,
time_now_s: int,
update: bool = True,
rate_hz: Optional[float] = None,
burst_count: 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
time_now_s: The current time
update: Whether to count this check as performing the 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.
Raises:
LimitExceededError: If an action could not be performed, along with the time in
milliseconds until the action can be performed again
"""
allowed, time_allowed = self.can_do_action(key, time_now_s, update)
# Override default values if set
rate_hz = rate_hz or self.rate_hz
burst_count = burst_count or self.burst_count

allowed, time_allowed = self.can_do_action(
key, time_now_s, update=update, rate_hz=rate_hz, burst_count=burst_count
)

if not allowed:
raise LimitExceededError(
Expand Down
14 changes: 8 additions & 6 deletions synapse/handlers/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ def __init__(self, hs):
self.clock = hs.get_clock()
self.hs = hs

self.ratelimiter = None
self.request_ratelimiter = hs.get_request_ratelimiter()
self._rc_message = self.hs.config.rc_message

Expand Down Expand Up @@ -103,14 +102,17 @@ def ratelimit(self, requester, update=True, is_admin_redaction=False):

if is_admin_redaction and self.admin_redaction_ratelimiter:
# If we have separate config for admin redactions, use a separate
# ratelimiter as to not have user_id's clash
# ratelimiter as to not have user_ids clash
self.admin_redaction_ratelimiter.ratelimit(user_id, time_now, update)
else:
# Override rate and burst count per-user
self.request_ratelimiter.rate_hz = messages_per_second
self.request_ratelimiter.burst_count = burst_count

self.request_ratelimiter.ratelimit(user_id, time_now, update)
self.request_ratelimiter.ratelimit(
user_id,
time_now,
update,
rate_hz=messages_per_second,
burst_count=burst_count,
)

async def maybe_kick_guest_users(self, event, context=None):
# Technically this function invalidates current_state by changing it.
Expand Down
3 changes: 1 addition & 2 deletions tests/handlers/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ def register_query_handler(query_type, handler):
federation_server=Mock(),
federation_registry=self.mock_registry,
request_ratelimiter=NonCallableMock(
# rate_hz and burst_count are overridden in BaseHandler
spec_set=["can_do_action", "ratelimit", "rate_hz", "burst_count"]
spec_set=["can_do_action", "ratelimit"]
),
login_ratelimiter=NonCallableMock(spec_set=["can_do_action", "ratelimit"]),
)
Expand Down
4 changes: 1 addition & 3 deletions tests/rest/client/v1/test_rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,12 @@ def make_homeserver(self, reactor, clock):
http_client=None,
federation_client=Mock(),
request_ratelimiter=NonCallableMock(
# rate_hz and burst_count are overridden in BaseHandler
spec_set=["can_do_action", "ratelimit", "rate_hz", "burst_count"]
spec_set=["can_do_action", "ratelimit"]
),
login_ratelimiter=NonCallableMock(spec_set=["can_do_action", "ratelimit"]),
)
self.request_ratelimiter = self.hs.get_request_ratelimiter()
self.request_ratelimiter.can_do_action.return_value = (True, 0)
self.request_ratelimiter.rate_hz = Mock()

self.login_ratelimiter = self.hs.get_login_ratelimiter()
self.login_ratelimiter.can_do_action.return_value = (True, 0)
Expand Down
3 changes: 1 addition & 2 deletions tests/rest/client/v1/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ def make_homeserver(self, reactor, clock):
http_client=None,
federation_client=Mock(),
request_ratelimiter=NonCallableMock(
# rate_hz and burst_count are overridden in BaseHandler
spec_set=["can_do_action", "ratelimit", "rate_hz", "burst_count"]
spec_set=["can_do_action", "ratelimit"]
),
login_ratelimiter=NonCallableMock(spec_set=["can_do_action", "ratelimit"]),
)
Expand Down

0 comments on commit f6203a6

Please sign in to comment.