From 0d4f46100001f639774e142012808b70d22cd8b4 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Thu, 12 Feb 2015 19:14:34 +0100 Subject: [PATCH] Very basic locking mechanism for quotas --- .../migrations/0008_quota_locked.py | 20 ++++++++++ src/pretixbase/models.py | 38 +++++++++++++++++-- src/pretixpresale/views/cart.py | 23 +++++++++-- 3 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 src/pretixbase/migrations/0008_quota_locked.py diff --git a/src/pretixbase/migrations/0008_quota_locked.py b/src/pretixbase/migrations/0008_quota_locked.py new file mode 100644 index 000000000..775d4fa8b --- /dev/null +++ b/src/pretixbase/migrations/0008_quota_locked.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0007_auto_20150212_0939'), + ] + + operations = [ + migrations.AddField( + model_name='quota', + name='locked', + field=models.DateTimeField(null=True, blank=True), + preserve_default=True, + ), + ] diff --git a/src/pretixbase/models.py b/src/pretixbase/models.py index cad790f55..deb1f36ea 100644 --- a/src/pretixbase/models.py +++ b/src/pretixbase/models.py @@ -1,6 +1,7 @@ from itertools import product import copy import uuid +import time from django.db import models from django.conf import settings @@ -1041,6 +1042,10 @@ class Quota(Versionable): blank=True, verbose_name=_("Variations") ) + locked = models.DateTimeField( + null=True, blank=True + ) + locked_here = False class Meta: verbose_name = _("Quota") @@ -1105,12 +1110,31 @@ class Quota(Versionable): return Quota.AVAILABILITY_OK, self.size - paid_orders - pending_valid_orders - valid_cart_positions + class LockTimeoutException(Exception): + pass + def lock(self): """ Issue a lock on this quota so nobody can take tickets from this quota until - you release the lock + you release the lock. + + Raises an Quota.LockTimeoutException if the quota is locked every time we + try to obtain a lock. """ - pass + retries = 5 + for i in range(retries): + dt = now() + updated = Quota.objects.current.filter( + identity=self.identity, locked__isnull=True, + version_end_date__isnull=True + ).update( + locked=dt + ) + if updated: + self.locked_here = dt + return True + time.sleep(2 ** i / 100) + raise Quota.LockTimeoutException() def release(self, force=False): """ @@ -1118,7 +1142,15 @@ class Quota(Versionable): the lock will only be released if it was issued in _this_ python representation of the database object. """ - pass + if not self.locked_here and not force: + return False + updated = Quota.objects.current.filter( + identity=self.identity, + version_end_date__isnull=True + ).update( + locked=None + ) + return updated class Order(Versionable): diff --git a/src/pretixpresale/views/cart.py b/src/pretixpresale/views/cart.py index ad8eed35c..bac687a28 100644 --- a/src/pretixpresale/views/cart.py +++ b/src/pretixpresale/views/cart.py @@ -94,13 +94,22 @@ class CartAdd(EventViewMixin, CartActionMixin, View): price = item.execute_restrictions() if variation is None else variation.execute_restrictions() if price is False: - msg_some_unavailable = True - messages.error(self.request, - _('Some of the items you selected were no longer available. ' - 'Please see below for details.')) + if not msg_some_unavailable: + msg_some_unavailable = True + messages.error(self.request, + _('Some of the items you selected were no longer available. ' + 'Please see below for details.')) continue quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all()) + if len(quotas) == 0: + if not msg_some_unavailable: + msg_some_unavailable = True + messages.error(self.request, + _('Some of the items you selected were no longer available. ' + 'Please see below for details.')) + continue + quota_ok = i[2] try: for quota in quotas: @@ -132,6 +141,12 @@ class CartAdd(EventViewMixin, CartActionMixin, View): price=price, expires=now() + timedelta(minutes=30) ) + except Quota.LockTimeoutException: + if not msg_some_unavailable: + msg_some_unavailable = True + messages.error(self.request, + _('We were not able to process your request completely as the ' + 'server was too busy. Please try again.')) finally: for quota in quotas: quota.release()