2017-11-16 00:43:27 +01:00
|
|
|
import logging
|
|
|
|
import time
|
2016-06-04 16:52:18 +02:00
|
|
|
from typing import Any, Callable, Optional
|
|
|
|
|
2013-05-31 19:47:09 +02:00
|
|
|
from django.conf import settings
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.core.management.base import BaseCommand, CommandError, CommandParser
|
2013-05-31 19:47:09 +02:00
|
|
|
|
2020-03-04 14:05:25 +01:00
|
|
|
from zerver.lib.rate_limiter import RateLimitedUser, client
|
2017-11-16 00:43:27 +01:00
|
|
|
from zerver.models import get_user_profile_by_id
|
2013-05-31 19:47:09 +02:00
|
|
|
|
2020-01-14 21:59:46 +01:00
|
|
|
|
2013-05-31 19:47:09 +02:00
|
|
|
class Command(BaseCommand):
|
2017-11-09 11:45:56 +01:00
|
|
|
help = """Checks redis to make sure our rate limiting system hasn't grown a bug
|
|
|
|
and left redis with a bunch of data
|
2013-05-31 19:47:09 +02:00
|
|
|
|
|
|
|
Usage: ./manage.py [--trim] check_redis"""
|
|
|
|
|
2017-10-26 11:35:57 +02:00
|
|
|
def add_arguments(self, parser: CommandParser) -> None:
|
2016-11-03 10:22:19 +01:00
|
|
|
parser.add_argument('-t', '--trim',
|
|
|
|
dest='trim',
|
|
|
|
default=False,
|
|
|
|
action='store_true',
|
|
|
|
help="Actually trim excess")
|
2013-05-31 19:47:09 +02:00
|
|
|
|
2017-10-26 11:35:57 +02:00
|
|
|
def _check_within_range(self, key: str, count_func: Callable[[], int],
|
|
|
|
trim_func: Optional[Callable[[str, int], None]]=None) -> None:
|
2013-05-31 19:47:09 +02:00
|
|
|
user_id = int(key.split(':')[1])
|
2018-05-17 22:12:23 +02:00
|
|
|
user = get_user_profile_by_id(user_id)
|
2017-07-31 08:00:57 +02:00
|
|
|
entity = RateLimitedUser(user)
|
2020-03-04 14:05:25 +01:00
|
|
|
max_calls = entity.max_api_calls()
|
2013-05-31 19:47:09 +02:00
|
|
|
|
2013-06-05 22:56:25 +02:00
|
|
|
age = int(client.ttl(key))
|
|
|
|
if age < 0:
|
2020-05-02 08:44:14 +02:00
|
|
|
logging.error("Found key with age of %s, will never expire: %s", age, key)
|
2013-06-05 22:44:22 +02:00
|
|
|
|
2013-05-31 19:47:09 +02:00
|
|
|
count = count_func()
|
|
|
|
if count > max_calls:
|
|
|
|
logging.error("Redis health check found key with more elements \
|
2020-05-02 08:44:14 +02:00
|
|
|
than max_api_calls! (trying to trim) %s %s", key, count)
|
2016-01-27 22:43:44 +01:00
|
|
|
if trim_func is not None:
|
2020-03-04 14:05:25 +01:00
|
|
|
client.expire(key, entity.max_api_window())
|
2013-05-31 19:47:09 +02:00
|
|
|
trim_func(key, max_calls)
|
|
|
|
|
2017-10-26 11:35:57 +02:00
|
|
|
def handle(self, *args: Any, **options: Any) -> None:
|
2013-05-31 19:47:09 +02:00
|
|
|
if not settings.RATE_LIMITING:
|
2019-05-03 23:20:39 +02:00
|
|
|
raise CommandError("This machine is not using redis or rate limiting, aborting")
|
2013-05-31 19:47:09 +02:00
|
|
|
|
|
|
|
# Find all keys, and make sure they're all within size constraints
|
|
|
|
wildcard_list = "ratelimit:*:*:list"
|
|
|
|
wildcard_zset = "ratelimit:*:*:zset"
|
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
trim_func: Optional[
|
|
|
|
Callable[[str, int], None]
|
|
|
|
] = lambda key, max_calls: client.ltrim(key, 0, max_calls - 1)
|
2016-01-27 22:43:44 +01:00
|
|
|
if not options['trim']:
|
|
|
|
trim_func = None
|
2013-05-31 19:47:09 +02:00
|
|
|
|
|
|
|
lists = client.keys(wildcard_list)
|
|
|
|
for list_name in lists:
|
|
|
|
self._check_within_range(list_name,
|
|
|
|
lambda: client.llen(list_name),
|
2016-01-27 22:43:44 +01:00
|
|
|
trim_func)
|
2013-05-31 19:47:09 +02:00
|
|
|
|
|
|
|
zsets = client.keys(wildcard_zset)
|
|
|
|
for zset in zsets:
|
|
|
|
now = time.time()
|
|
|
|
# We can warn on our zset being too large, but we don't know what
|
|
|
|
# elements to trim. We'd have to go through every list item and take
|
|
|
|
# the intersection. The best we can do is expire it
|
|
|
|
self._check_within_range(zset,
|
2017-01-24 06:21:14 +01:00
|
|
|
lambda: client.zcount(zset, 0, now),
|
|
|
|
lambda key, max_calls: None)
|