forked from CGM_Public/pretix_original
Added the possibility to use Redis for quota locks
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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):
|
||||||
|
|||||||
111
src/pretix/base/services/locking.py
Normal file
111
src/pretix/base/services/locking.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
2
src/requirements/redis.txt
Normal file
2
src/requirements/redis.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
django-redis>=4.1,<4.2
|
||||||
|
redis>=2.10,<2.11
|
||||||
Reference in New Issue
Block a user