Locking optimizations

This commit is contained in:
Raphael Michel
2019-05-05 16:08:41 +02:00
parent 32e66aeb55
commit 4e769ba11e
4 changed files with 59 additions and 17 deletions

View File

@@ -32,6 +32,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import User from pretix.base.models import User
from pretix.base.reldate import RelativeDateWrapper from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.locking import NoLockManager
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
from .base import LockModel, LoggedModel from .base import LockModel, LoggedModel
@@ -1222,13 +1223,13 @@ class OrderPayment(models.Model):
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock: if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(hours=12)) or not lock:
# Performance optimization. In this case, there's really no reason to lock everything and an atomic # Performance optimization. In this case, there's really no reason to lock everything and an atomic
# database transaction is more than enough. # database transaction is more than enough.
with transaction.atomic(): lockfn = NoLockManager
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total,
ignore_date=ignore_date)
else: else:
with self.order.event.lock(): lockfn = self.order.event.lock
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total,
ignore_date=ignore_date) with lockfn():
self._mark_paid(force, count_waitinglist, user, auth, overpaid=payment_sum - refund_sum > self.order.total,
ignore_date=ignore_date)
invoice = None invoice = None
if invoice_qualified(self.order): if invoice_qualified(self.order):

View File

@@ -5,7 +5,7 @@ from typing import List, Optional
from celery.exceptions import MaxRetriesExceededError from celery.exceptions import MaxRetriesExceededError
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import DatabaseError, transaction
from django.db.models import Q from django.db.models import Q
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.timezone import make_aware, now from django.utils.timezone import make_aware, now
@@ -21,7 +21,7 @@ from pretix.base.models.orders import OrderFee
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.reldate import RelativeDateWrapper from pretix.base.reldate import RelativeDateWrapper
from pretix.base.services.checkin import _save_answers from pretix.base.services.checkin import _save_answers
from pretix.base.services.locking import LockTimeoutException from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.pricing import get_price from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledTask from pretix.base.services.tasks import ProfiledTask
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -791,7 +791,11 @@ class CartManager:
if available_count == 1: if available_count == 1:
op.position.expires = self._expiry op.position.expires = self._expiry
op.position.price = op.price.gross op.position.price = op.price.gross
op.position.save() try:
op.position.save(force_update=True)
except DatabaseError:
# Best effort... The position might have been deleted in the meantime!
pass
elif available_count == 0: elif available_count == 0:
op.position.addons.all().delete() op.position.addons.all().delete()
op.position.delete() op.position.delete()
@@ -806,17 +810,33 @@ class CartManager:
CartPosition.objects.bulk_create([p for p in new_cart_positions if not getattr(p, '_answers', None) and not p.pk]) CartPosition.objects.bulk_create([p for p in new_cart_positions if not getattr(p, '_answers', None) and not p.pk])
return err return err
def _require_locking(self):
if self._voucher_use_diff:
# If any vouchers are used, we lock to make sure we don't redeem them to often
return True
if self._quota_diff and any(q.size is not None for q in self._quota_diff):
# If any quotas are affected that are not unlimited, we lock
return True
return False
def commit(self): def commit(self):
self._check_presale_dates() self._check_presale_dates()
self._check_max_cart_size() self._check_max_cart_size()
self._calculate_expiry() self._calculate_expiry()
with self.event.lock() as now_dt: err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
lockfn = NoLockManager
if self._require_locking():
lockfn = self.event.lock
with lockfn() as now_dt:
with transaction.atomic(): with transaction.atomic():
self.now_dt = now_dt self.now_dt = now_dt
self._extend_expiry_of_valid_existing_positions() self._extend_expiry_of_valid_existing_positions()
err = self._delete_out_of_timeframe()
err = self.extend_expired_positions() or err
err = self._perform_operations() or err err = self._perform_operations() or err
if err: if err:
raise CartError(err) raise CartError(err)

View File

@@ -13,6 +13,18 @@ logger = logging.getLogger('pretix.base.locking')
LOCK_TIMEOUT = 120 LOCK_TIMEOUT = 120
class NoLockManager:
def __init__(self):
pass
def __enter__(self):
return now()
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
return False
class LockManager: class LockManager:
def __init__(self, event): def __init__(self, event):
self.event = event self.event = event

View File

@@ -39,7 +39,7 @@ from pretix.base.services import tickets
from pretix.base.services.invoices import ( from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_qualified, generate_cancellation, generate_invoice, invoice_qualified,
) )
from pretix.base.services.locking import LockTimeoutException from pretix.base.services.locking import LockTimeoutException, NoLockManager
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
from pretix.base.services.pricing import get_price from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import ProfiledTask from pretix.base.services.tasks import ProfiledTask
@@ -665,9 +665,18 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
except InvoiceAddress.DoesNotExist: except InvoiceAddress.DoesNotExist:
pass pass
with event.lock() as now_dt: positions = CartPosition.objects.filter(id__in=position_ids, event=event)
positions = list(CartPosition.objects.filter(
id__in=position_ids).select_related('item', 'variation', 'subevent', 'addon_to').prefetch_related('addons')) lockfn = NoLockManager
locked = False
if positions.filter(Q(voucher__isnull=False) | Q(expires__lt=now() + timedelta(minutes=2))).exists():
# Performance optimization: If no voucher is used and no cart position is dangerously close to its expiry date,
# creating this order shouldn't be prone to any race conditions and we don't need to lock the event.
locked = True
lockfn = event.lock
with lockfn() as now_dt:
positions = list(positions.select_related('item', 'variation', 'subevent', 'addon_to').prefetch_related('addons'))
if len(positions) == 0: if len(positions) == 0:
raise OrderError(error_messages['empty']) raise OrderError(error_messages['empty'])
if len(position_ids) != len(positions): if len(position_ids) != len(positions):
@@ -679,7 +688,7 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
free_order_flow = payment and payment_provider == 'free' and order.total == Decimal('0.00') and not order.require_approval free_order_flow = payment and payment_provider == 'free' and order.total == Decimal('0.00') and not order.require_approval
if free_order_flow: if free_order_flow:
try: try:
payment.confirm(send_mail=False, lock=False) payment.confirm(send_mail=False, lock=not locked)
except Quota.QuotaExceededException: except Quota.QuotaExceededException:
pass pass