Fixed and improved locking implementations

This commit is contained in:
Raphael Michel
2015-09-28 22:59:28 +02:00
parent 06868d6d17
commit 72ecfea622
3 changed files with 52 additions and 19 deletions

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import uuid
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0017_order_guest_locale'),
]
operations = [
migrations.AddField(
model_name='eventlock',
name='token',
field=models.UUIDField(default=uuid.uuid4),
),
]

View File

@@ -366,7 +366,6 @@ class Event(Versionable):
null=True, blank=True, null=True, blank=True,
verbose_name=_("Plugins"), verbose_name=_("Plugins"),
) )
locked_here = False
class Meta: class Meta:
verbose_name = _("Event") verbose_name = _("Event")
@@ -1822,6 +1821,10 @@ class OrganizerSetting(Versionable):
class EventLock(models.Model): class EventLock(models.Model):
event = models.CharField(max_length=36, primary_key=True) event = models.CharField(max_length=36, primary_key=True)
date = models.DateTimeField(auto_now=True) date = models.DateTimeField(auto_now=True)
token = models.UUIDField(default=uuid.uuid4)
class LockTimeoutException(Exception): class LockTimeoutException(Exception):
pass pass
class LockReleaseException(Exception):
pass

View File

@@ -1,6 +1,7 @@
import logging import logging
import time import time
from datetime import timedelta from datetime import timedelta
import uuid
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
@@ -9,6 +10,7 @@ from django.utils.timezone import now
from pretix.base.models import EventLock from pretix.base.models import EventLock
logger = logging.getLogger('pretix.base.locking') logger = logging.getLogger('pretix.base.locking')
LOCK_TIMEOUT = 120
class LockManager: class LockManager:
@@ -32,22 +34,25 @@ def lock_event(event):
:raises EventLock.LockTimeoutException: if the event is locked every time we try :raises EventLock.LockTimeoutException: if the event is locked every time we try
to obtain the lock to obtain the lock
""" """
if event.locked_here: if hasattr(event, '_lock') and event._lock:
return True return True
if settings.HAS_REDIS: if settings.HAS_REDIS:
return lock_event_redis(event) return lock_event_redis(event)
else: else:
return lock_event_db(event) return lock_event_db(event)
def release_event(event, force=False): def release_event(event):
""" """
Release a lock placed by :py:meth:`lock()`. If the parameter force is not set to ``True``, 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 the lock will only be released if it was issued in _this_ python
representation of the database object. representation of the database object.
:raises EventLock.LockReleaseException: if we do not own the lock
""" """
if not event.locked_here and not force: if not hasattr(event, '_lock') or not event._lock:
return False raise EventLock.LockReleaseException('')
if settings.HAS_REDIS: if settings.HAS_REDIS:
return release_event_redis(event) return release_event_redis(event)
else: else:
@@ -61,31 +66,39 @@ def lock_event_db(event):
dt = now() dt = now()
l, created = EventLock.objects.get_or_create(event=event.identity) l, created = EventLock.objects.get_or_create(event=event.identity)
if created: if created:
event.locked_here = dt event._lock = l
return True return True
elif l.date < now() - timedelta(seconds=120): elif l.date < now() - timedelta(seconds=LOCK_TIMEOUT):
updated = EventLock.objects.filter(event=event.identity, date=l.date).update(date=dt) newtoken = uuid.uuid4()
updated = EventLock.objects.filter(event=event.identity, token=l.token).update(date=dt, token=newtoken)
if updated: if updated:
event.locked_here = dt l.token = newtoken
event._lock = l
return True return True
time.sleep(2 ** i / 100) time.sleep(2 ** i / 100)
raise EventLock.LockTimeoutException() raise EventLock.LockTimeoutException()
@transaction.atomic()
def release_event_db(event): def release_event_db(event):
deleted = EventLock.objects.filter(event=event.identity).delete() if not hasattr(event, '_lock') or not event._lock:
event.locked_here = None raise EventLock.LockReleaseException('Lock is not owned by this thread')
return deleted try:
lock = EventLock.objects.get(event=event.identity, token=event._lock.token)
lock.delete()
event._lock = None
except EventLock.DoesNotExist:
raise EventLock.LockReleaseException('Lock is no longer owned by this thread')
def redis_lock_from_event(event): def redis_lock_from_event(event):
from django_redis import get_redis_connection from django_redis import get_redis_connection
from redis.lock import Lock from redis.lock import Lock
if not hasattr(event, '_redis_lock'): if not hasattr(event, '_lock') or not event._lock:
rc = get_redis_connection("redis") rc = get_redis_connection("redis")
event._redis_lock = Lock(redis=rc, name='pretix_event_%s' % event.identity, timeout=120) event._lock = Lock(redis=rc, name='pretix_event_%s' % event.identity, timeout=LOCK_TIMEOUT)
return event._redis_lock return event._lock
def lock_event_redis(event): def lock_event_redis(event):
@@ -94,10 +107,8 @@ def lock_event_redis(event):
lock = redis_lock_from_event(event) lock = redis_lock_from_event(event)
retries = 5 retries = 5
for i in range(retries): for i in range(retries):
dt = now()
try: try:
if lock.acquire(False): if lock.acquire(False):
event.locked_here = dt
return True return True
except RedisError: except RedisError:
logger.exception('Error locking an event') logger.exception('Error locking an event')
@@ -115,5 +126,4 @@ def release_event_redis(event):
except RedisError: except RedisError:
logger.exception('Error releasing an event lock') logger.exception('Error releasing an event lock')
raise EventLock.LockTimeoutException() raise EventLock.LockTimeoutException()
event.locked_here = None event._lock = None
return True