New locking mechanism (#2408)

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2023-09-11 11:44:50 +02:00
committed by GitHub
parent b2b3fa36be
commit c842ea597c
33 changed files with 1638 additions and 883 deletions

View File

@@ -25,6 +25,7 @@ from typing import List
from django.db import transaction
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext as _
from rest_framework import status, viewsets
from rest_framework.decorators import action
@@ -41,7 +42,7 @@ from pretix.base.models import CartPosition
from pretix.base.services.cart import (
_get_quota_availability, _get_voucher_availability, error_messages,
)
from pretix.base.services.locking import NoLockManager
from pretix.base.services.locking import lock_objects
class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
@@ -150,12 +151,21 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
quota_diff[q] += 1
seats_seen = set()
now_dt = now()
with transaction.atomic():
full_lock_required = seat_diff and self.request.event.settings.seating_minimal_distance > 0
if full_lock_required:
# We lock the entire event in this case since we don't want to deal with fine-granular locking
# in the case of seating distance enforcement
lock_objects([self.request.event])
else:
lock_objects(
[q for q, d in quota_diff.items() if q.size is not None and d > 0] +
[v for v, d in voucher_use_diff.items() if d > 0] +
[s for s, d in seat_diff.items() if d > 0],
shared_lock_objects=[self.request.event]
)
lockfn = NoLockManager
if self._require_locking(quota_diff, voucher_use_diff, seat_diff):
lockfn = self.request.event.lock
with lockfn() as now_dt, transaction.atomic():
vouchers_ok, vouchers_depend_on_cart = _get_voucher_availability(
self.request.event,
voucher_use_diff,

View File

@@ -19,8 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import contextlib
from django.db import transaction
from django.db.models import F, Q
from django.utils.timezone import now
@@ -69,30 +67,9 @@ class VoucherViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return self.request.event.vouchers.select_related('seat').all()
def _predict_quota_check(self, data, instance):
# This method predicts if Voucher.clean_quota_needs_checking
# *migh* later require a quota check. It is only approximate
# and returns True a little too often. The point is to avoid
# locks when we know we won't need them.
if 'allow_ignore_quota' in data and data.get('allow_ignore_quota'):
return False
if instance and 'allow_ignore_quota' not in data and instance.allow_ignore_quota:
return False
if 'block_quota' in data and not data.get('block_quota'):
return False
if instance and 'block_quota' not in data and not instance.block_quota:
return False
return True
@transaction.atomic()
def create(self, request, *args, **kwargs):
if self._predict_quota_check(request.data, None):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
return super().create(request, *args, **kwargs)
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(event=self.request.event)
@@ -108,13 +85,9 @@ class VoucherViewSet(viewsets.ModelViewSet):
ctx['event'] = self.request.event
return ctx
@transaction.atomic()
def update(self, request, *args, **kwargs):
if self._predict_quota_check(request.data, self.get_object()):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
return super().update(request, *args, **kwargs)
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
@@ -140,22 +113,18 @@ class VoucherViewSet(viewsets.ModelViewSet):
super().perform_destroy(instance)
@action(detail=False, methods=['POST'])
@transaction.atomic()
def batch_create(self, request, *args, **kwargs):
if any(self._predict_quota_check(d, None) for d in request.data):
lockfn = request.event.lock
else:
lockfn = contextlib.suppress # noop context manager
with lockfn():
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
serializer.save(event=self.request.event)
for i, v in enumerate(serializer.instance):
v.log_action(
'pretix.voucher.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data[i]
)
serializer = self.get_serializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
with transaction.atomic():
serializer.save(event=self.request.event)
for i, v in enumerate(serializer.instance):
v.log_action(
'pretix.voucher.added',
user=self.request.user,
auth=self.request.auth,
data=self.request.data[i]
)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)