2013-05-31 19:47:09 +02:00
|
|
|
from __future__ import absolute_import
|
2015-11-01 17:11:06 +01:00
|
|
|
from __future__ import print_function
|
2013-05-31 19:47:09 +02:00
|
|
|
|
2016-06-04 16:52:18 +02:00
|
|
|
from typing import Any, Callable, Optional
|
|
|
|
|
2013-07-29 23:03:31 +02:00
|
|
|
from zerver.models import get_user_profile_by_id
|
|
|
|
from zerver.lib.rate_limiter import client, max_api_calls, max_api_window
|
2013-05-31 19:47:09 +02:00
|
|
|
|
2016-11-03 10:22:19 +01:00
|
|
|
from django.core.management.base import BaseCommand, CommandParser
|
2013-05-31 19:47:09 +02:00
|
|
|
from django.conf import settings
|
|
|
|
from optparse import make_option
|
|
|
|
|
2017-01-24 06:36:39 +01:00
|
|
|
import logging
|
|
|
|
import time
|
2013-05-31 19:47:09 +02:00
|
|
|
|
|
|
|
class Command(BaseCommand):
|
|
|
|
help = """Checks redis to make sure our rate limiting system hasn't grown a bug and left redis with a bunch of data
|
|
|
|
|
|
|
|
Usage: ./manage.py [--trim] check_redis"""
|
|
|
|
|
2016-11-03 10:22:19 +01:00
|
|
|
def add_arguments(self, parser):
|
|
|
|
# type: (CommandParser) -> None
|
|
|
|
parser.add_argument('-t', '--trim',
|
|
|
|
dest='trim',
|
|
|
|
default=False,
|
|
|
|
action='store_true',
|
|
|
|
help="Actually trim excess")
|
2013-05-31 19:47:09 +02:00
|
|
|
|
2016-01-27 22:43:44 +01:00
|
|
|
def _check_within_range(self, key, count_func, trim_func=None):
|
2016-09-12 08:07:02 +02:00
|
|
|
# type: (str, Callable[[], int], Optional[Callable[[str, int], None]]) -> None
|
2013-05-31 19:47:09 +02:00
|
|
|
user_id = int(key.split(':')[1])
|
|
|
|
try:
|
|
|
|
user = get_user_profile_by_id(user_id)
|
2017-03-05 10:25:27 +01:00
|
|
|
except Exception:
|
2013-05-31 19:47:09 +02:00
|
|
|
user = None
|
|
|
|
max_calls = max_api_calls(user=user)
|
|
|
|
|
2013-06-05 22:56:25 +02:00
|
|
|
age = int(client.ttl(key))
|
|
|
|
if age < 0:
|
|
|
|
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 \
|
|
|
|
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:
|
2013-06-05 22:44:22 +02:00
|
|
|
client.expire(key, max_api_window(user=user))
|
2013-05-31 19:47:09 +02:00
|
|
|
trim_func(key, max_calls)
|
|
|
|
|
|
|
|
def handle(self, *args, **options):
|
2016-06-04 16:52:18 +02:00
|
|
|
# type: (*Any, **Any) -> None
|
2013-05-31 19:47:09 +02:00
|
|
|
if not settings.RATE_LIMITING:
|
2015-11-01 17:11:06 +01:00
|
|
|
print("This machine is not using redis or rate limiting, aborting")
|
2013-05-31 19:47:09 +02:00
|
|
|
exit(1)
|
|
|
|
|
|
|
|
# Find all keys, and make sure they're all within size constraints
|
|
|
|
wildcard_list = "ratelimit:*:*:list"
|
|
|
|
wildcard_zset = "ratelimit:*:*:zset"
|
|
|
|
|
2016-01-27 22:43:44 +01:00
|
|
|
trim_func = lambda key, max_calls: client.ltrim(key, 0, max_calls - 1)
|
|
|
|
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)
|