Inclusion de un ratelimit usando cache

Necesitaba un ratelimit que estuviera disponible en todos los workers,
cosa que aparentemente la libreria que estaba usando no tomaba en cuenta

asi que la mejor idea que tuve es aprovechar el cache y usarlo para
generar el ratelimit :3 asi que como todos los workers se comunican con
el mismo cache, todos van a compartir el mismo lock
This commit is contained in:
Daniel Cortes
2020-06-08 19:14:51 -04:00
parent b71b6e824d
commit 392dba39ea
6 changed files with 44 additions and 10 deletions

View File

@@ -1,14 +1,18 @@
#!/usr/bin/env sh #!/usr/bin/env sh
set -eu set -eu
# If venv doesnt exists create it # If venv exists delete it
[ -d venv/ ] || python3 -m venv venv [ -d venv/ ] && rm venv/
# Create a new venv
python3 -m venv venv
# Activate venv # Activate venv
. venv/bin/activate . venv/bin/activate
# Install all requirements # Install all requirements
pip install -r requirements.txt -r requirements-dev.txt pip install -r requirements.txt -r requirements-dev.txt
# Collect static files # Collect static files
./manage.py collectstatic --noinput ./manage.py collectstatic --noinput

View File

@@ -6,8 +6,7 @@ musicbrainz entrega exceptuando los errores.
from urllib.parse import quote, urlencode from urllib.parse import quote, urlencode
import logging import logging
import requests import requests
from ratelimit import limits, sleep_and_retry from utils import sanitize_keys, ratelimit
from utils import sanitize_keys
HEADERS = {'accept': 'application/json', 'user-agent': 'MusicList/1.0 (danielcortes.xyz)'} HEADERS = {'accept': 'application/json', 'user-agent': 'MusicList/1.0 (danielcortes.xyz)'}
MB_HOST = 'https://musicbrainz.org/ws/2' MB_HOST = 'https://musicbrainz.org/ws/2'
@@ -37,8 +36,6 @@ def _do_request(url):
return response return response
@sleep_and_retry
@limits(calls=1, period=1)
def _do_request_mb(url): def _do_request_mb(url):
"""Does a request to a path of musicbrainz and returns the dictionary representing the json """Does a request to a path of musicbrainz and returns the dictionary representing the json
response response
@@ -52,6 +49,7 @@ def _do_request_mb(url):
:return: The dictionary with the response or the status and his an error message :return: The dictionary with the response or the status and his an error message
""" """
ratelimit()
response = _do_request(url) response = _do_request(url)
if response.status_code == 200: if response.status_code == 200:

View File

@@ -149,7 +149,7 @@ LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'formatters': { 'formatters': {
'base': {'format': '[{levelname}]\t({name}): {message}', 'style': '{'} 'base': {'format': '[{levelname}]({asctime})({name}): {message}', 'style': '{'}
}, },
'handlers': { 'handlers': {

View File

@@ -3,7 +3,6 @@ django-cors-middleware # To handle cors request
django-oauth-toolkit # To handle user authentication django-oauth-toolkit # To handle user authentication
djangorestframework # The framework to create the api djangorestframework # The framework to create the api
pygments # To make cute pretty prints XD pygments # To make cute pretty prints XD
ratelimit # To rate limit request to musicbrainz
redis # To comunicate with redis redis # To comunicate with redis
django-redis # Redis backend for django django-redis # Redis backend for django
django-rq #Job queue django-rq #Job queue

View File

@@ -10,7 +10,7 @@ from django.http import JsonResponse
from pygments import highlight from pygments import highlight
from pygments.lexers import JsonLexer # pylint: disable=no-name-in-module from pygments.lexers import JsonLexer # pylint: disable=no-name-in-module
from pygments.formatters import TerminalTrueColorFormatter # pylint: disable=no-name-in-module from pygments.formatters import TerminalTrueColorFormatter # pylint: disable=no-name-in-module
from utils.ratelimit import ratelimit
_log = logging.getLogger('utils') _log = logging.getLogger('utils')
_log.addHandler(logging.NullHandler()) _log.addHandler(logging.NullHandler())

33
utils/ratelimit.py Normal file
View File

@@ -0,0 +1,33 @@
"""Modulo encargado de la función de ratelimit"""
import time
import logging
from django.core.cache import cache
from django.utils import timezone
_log = logging.getLogger('utils_ratelimit')
_log.addHandler(logging.NullHandler())
def ratelimit():
"""Rate limit básico, espera un segundo entre ejecuciones
Esto lo logra a través del cache, utilizando la key musicbrainz_ratelimit_SS en donde SS es el
segundo actual, si es que ya esta seteada la key, se esperaran 0.001 segundos, y se volverá a
intentar
"""
while not cache.set(
f'musicbrainz_ratelimit_{timezone.now().second}',
'locked <3',
nx=True,
timeout=10):
# Le tengo algo de respeto a este sleep, si es demasiado corto igual y es un busy
# loop y eso es horrible para el rendimiento, pero si es muy largo igual pierdo demasiadas
# oportunidades de obtener un lock.
# Ademas la precision de sleep depende del sistema operativo, supuestamente linux es mejor
# para llevar esto, pero no estoy nada seguro~
# https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep
time.sleep(0.001)
_log.debug('Ratelimit allowing request on second %s', timezone.now().second)