Added the possibility to use Redis for quota locks

This commit is contained in:
Raphael Michel
2015-06-30 16:48:00 +02:00
parent 77905c596b
commit 235769ce4f
6 changed files with 165 additions and 32 deletions

View File

@@ -27,6 +27,7 @@ RUN pip3 install -r requirements.txt
RUN pip3 install -r requirements/mysql.txt RUN pip3 install -r requirements/mysql.txt
RUN pip3 install -r requirements/postgres.txt RUN pip3 install -r requirements/postgres.txt
RUN pip3 install -r requirements/memcached.txt RUN pip3 install -r requirements/memcached.txt
RUN pip3 install -r requirements/redis.txt
RUN pip3 install gunicorn RUN pip3 install gunicorn
RUN make RUN make

View File

@@ -177,4 +177,25 @@ You can use an existing memcached server as pretix's caching backend::
If no memcached is configures, pretix will use Django's built-in local-memory caching method. If no memcached is configures, pretix will use Django's built-in local-memory caching method.
Redis
-----
If a redis server is configured, pretix can use it for locking, caching and session storage
to speed up various operations::
[redis]
location=redis://127.0.0.1:6379/1
sessions=false
``location``
The location of memcached, as an URL of the form ``redis://[:password]@localhost:6379/0``
or ``unix://[:password]@/path/to/socket.sock?db=0``
``session``
When this is set to true, redis will be used as the session storage.
If no redis is configured, pretix will store sessions and locks in the database. If memcached
is configured, memcached will be used for caching instead of redis.
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure .. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure

View File

@@ -1408,22 +1408,8 @@ class Quota(Versionable):
:raises Quota.LockTimeoutException: if the quota is locked every time we try :raises Quota.LockTimeoutException: if the quota is locked every time we try
to obtain the lock to obtain the lock
""" """
retries = 5 from .services import locking
for i in range(retries): return locking.lock_quota(self)
dt = now()
updated = Quota.objects.current.filter(
Q(identity=self.identity)
& Q(Q(locked__lt=dt - timedelta(seconds=120)) | Q(locked__isnull=True))
& Q(version_end_date__isnull=True)
).update(
locked=dt
)
if updated:
self.locked_here = dt
self.locked = dt
return True
time.sleep(2 ** i / 100)
raise Quota.LockTimeoutException()
def release(self, force=False): def release(self, force=False):
""" """
@@ -1431,17 +1417,8 @@ class Quota(Versionable):
the lock will only be released if it was issued in _this_ python the lock will only be released if it was issued in _this_ python
representation of the database object. representation of the database object.
""" """
if not self.locked_here and not force: from .services import locking
return False return locking.release_quota(self, force)
updated = Quota.objects.current.filter(
identity=self.identity,
version_end_date__isnull=True
).update(
locked=None
)
self.locked_here = None
self.locked = None
return updated
class Order(Versionable): class Order(Versionable):

View File

@@ -0,0 +1,111 @@
from datetime import timedelta
import logging
import time
from django.db.models import Q
from django.utils.timezone import now
from pretix import settings
from pretix.base.models import Quota
from redis import RedisError
logger = logging.getLogger('pretix.base.locking')
def lock_quota(quota):
"""
Issue a lock on this quota so nobody can take tickets from this quota until
you release the lock. Will retry 5 times on failure.
:raises Quota.LockTimeoutException: if the quota is locked every time we try
to obtain the lock
"""
if settings.HAS_REDIS:
return lock_quota_redis(quota)
else:
return lock_quota_db(quota)
def lock_quota_db(quota):
retries = 5
for i in range(retries):
dt = now()
updated = Quota.objects.current.filter(
Q(identity=quota.identity)
& Q(Q(locked__lt=dt - timedelta(seconds=120)) | Q(locked__isnull=True))
& Q(version_end_date__isnull=True)
).update(
locked=dt
)
if updated:
quota.locked_here = dt
quota.locked = dt
return True
time.sleep(2 ** i / 100)
raise Quota.LockTimeoutException()
def release_quota(quota, force=False):
"""
Release a lock placed by :py:meth:`lock()`. If the parameter force is not set to ``True``,
the lock will only be released if it was issued in _this_ python
representation of the database object.
"""
if not quota.locked_here and not force:
return False
if settings.HAS_REDIS:
return release_quota_redis(quota)
else:
return release_quota_db(quota)
def release_quota_db(quota):
updated = Quota.objects.current.filter(
identity=quota.identity,
version_end_date__isnull=True
).update(
locked=None
)
quota.locked_here = None
quota.locked = None
return updated
def redis_lock_from_quota(quota):
from django_redis import get_redis_connection
from redis.lock import Lock
if not hasattr(quota, '_redis_lock'):
rc = get_redis_connection("redis")
quota._redis_lock = Lock(redis=rc, name='pretix_quota_%s' % quota.identity, timeout=120)
return quota._redis_lock
def lock_quota_redis(quota):
from redis.exceptions import RedisError
lock = redis_lock_from_quota(quota)
retries = 5
for i in range(retries):
dt = now()
try:
if lock.acquire(False):
quota.locked_here = dt
quota.locked = dt
return True
except RedisError:
logger.exception('Error locking a quota')
raise Quota.LockTimeoutException()
time.sleep(2 ** i / 100)
raise Quota.LockTimeoutException()
def release_quota_redis(quota):
lock = redis_lock_from_quota(quota)
try:
lock.release()
except RedisError:
logger.exception('Error releasing a quota lock')
raise Quota.LockTimeoutException()
quota.locked_here = None
quota.locked = None
return True

View File

@@ -76,13 +76,34 @@ SESSION_COOKIE_SECURE = SESSION_COOKIE_HTTPONLY = config.getboolean(
LANGUAGE_COOKIE_DOMAIN = SESSION_COOKIE_DOMAIN = CSRF_COOKIE_DOMAIN = config.get( LANGUAGE_COOKIE_DOMAIN = SESSION_COOKIE_DOMAIN = CSRF_COOKIE_DOMAIN = config.get(
'pretix', 'cookiedomain', fallback=None) 'pretix', 'cookiedomain', fallback=None)
if config.has_option('memcached', 'location'): CACHES = {
CACHES = { 'default': {
'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', 'LOCATION': 'unique-snowflake',
'LOCATION': config.get('memcached', 'location'), }
}
HAS_MEMCACHED = config.has_option('memcached', 'location')
if HAS_MEMCACHED:
CACHES['default'] = {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
'LOCATION': config.get('memcached', 'location'),
}
HAS_REDIS = config.has_option('redis', 'location')
if HAS_REDIS:
CACHES['redis'] = {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": config.get('redis', 'location'),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
} }
} }
if not HAS_MEMCACHED:
CACHES['default'] = CACHES['redis']
if config.getboolean('redis', 'sessions', fallback=False):
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "redis"
# Internal settings # Internal settings

View File

@@ -0,0 +1,2 @@
django-redis>=4.1,<4.2
redis>=2.10,<2.11