forked from CGM_Public/pretix_original
New locking mechanism (#2408)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -743,12 +743,7 @@ class Event(EventMixin, LoggedModel):
|
||||
return ObjectRelatedCache(self)
|
||||
|
||||
def lock(self):
|
||||
"""
|
||||
Returns a contextmanager that can be used to lock an event for bookings.
|
||||
"""
|
||||
from pretix.base.services import locking
|
||||
|
||||
return locking.LockManager(self)
|
||||
raise NotImplementedError("this method has been removed")
|
||||
|
||||
def get_mail_backend(self, timeout=None):
|
||||
"""
|
||||
|
||||
@@ -37,10 +37,13 @@ import copy
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
import string
|
||||
from collections import Counter
|
||||
from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from functools import reduce
|
||||
from time import sleep
|
||||
from typing import Any, Dict, List, Union
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
@@ -75,7 +78,6 @@ from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Customer, User
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.services.locking import LOCK_TIMEOUT, NoLockManager
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import order_gracefully_delete
|
||||
|
||||
@@ -83,6 +85,7 @@ from ...helpers import OF_SELF
|
||||
from ...helpers.countries import CachedCountries, FastCountryField
|
||||
from ...helpers.format import format_map
|
||||
from ...helpers.names import build_name
|
||||
from ...testutils.middleware import debugflags_var
|
||||
from ._transactions import (
|
||||
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
|
||||
)
|
||||
@@ -923,7 +926,7 @@ class Order(LockModel, LoggedModel):
|
||||
else:
|
||||
return expires
|
||||
|
||||
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
|
||||
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False, lock=False) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
||||
"payment settings is over."),
|
||||
@@ -944,10 +947,11 @@ class Order(LockModel, LoggedModel):
|
||||
if not self.event.settings.get('payment_term_accept_late') and not ignore_date and not force:
|
||||
return error_messages['late']
|
||||
|
||||
return self._is_still_available(count_waitinglist=count_waitinglist, force=force)
|
||||
return self._is_still_available(count_waitinglist=count_waitinglist, force=force, lock=lock)
|
||||
|
||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, force=False,
|
||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True, lock=False, force=False,
|
||||
check_voucher_usage=False, check_memberships=False) -> Union[bool, str]:
|
||||
from pretix.base.services.locking import lock_objects
|
||||
from pretix.base.services.memberships import (
|
||||
validate_memberships_in_order,
|
||||
)
|
||||
@@ -966,10 +970,21 @@ class Order(LockModel, LoggedModel):
|
||||
try:
|
||||
if check_memberships:
|
||||
try:
|
||||
validate_memberships_in_order(self.customer, positions, self.event, lock=False, testmode=self.testmode)
|
||||
validate_memberships_in_order(self.customer, positions, self.event, lock=lock, testmode=self.testmode)
|
||||
except ValidationError as e:
|
||||
raise Quota.QuotaExceededException(e.message)
|
||||
|
||||
for cp in positions:
|
||||
cp._cached_quotas = list(cp.quotas) if not force else []
|
||||
|
||||
if lock:
|
||||
lock_objects(
|
||||
[q for q in reduce(operator.or_, (set(cp._cached_quotas) for cp in positions), set()) if q.size is not None] +
|
||||
[op.voucher for op in positions if op.voucher and not force] +
|
||||
[op.seat for op in positions if op.seat],
|
||||
shared_lock_objects=[self.event]
|
||||
)
|
||||
|
||||
for i, op in enumerate(positions):
|
||||
if op.seat:
|
||||
if not op.seat.is_available(ignore_orderpos=op):
|
||||
@@ -994,7 +1009,7 @@ class Order(LockModel, LoggedModel):
|
||||
voucher=op.voucher.code
|
||||
))
|
||||
|
||||
quotas = list(op.quotas)
|
||||
quotas = op._cached_quotas
|
||||
if len(quotas) == 0:
|
||||
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
||||
item=str(op.item) + (' - ' + str(op.variation) if op.variation else '')
|
||||
@@ -1016,6 +1031,9 @@ class Order(LockModel, LoggedModel):
|
||||
))
|
||||
except Quota.QuotaExceededException as e:
|
||||
return str(e)
|
||||
|
||||
if 'sleep-after-quota-check' in debugflags_var.get():
|
||||
sleep(2)
|
||||
return True
|
||||
|
||||
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
|
||||
@@ -1647,9 +1665,10 @@ class OrderPayment(models.Model):
|
||||
return self.order.event.get_payment_providers(cached=True).get(self.provider)
|
||||
|
||||
@transaction.atomic()
|
||||
def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False):
|
||||
def _mark_paid_inner(self, force, count_waitinglist, user, auth, ignore_date=False, overpaid=False, lock=False):
|
||||
from pretix.base.signals import order_paid
|
||||
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force)
|
||||
can_be_paid = self.order._can_be_paid(count_waitinglist=count_waitinglist, ignore_date=ignore_date, force=force,
|
||||
lock=lock)
|
||||
if can_be_paid is not True:
|
||||
self.order.log_action('pretix.event.order.quotaexceeded', {
|
||||
'message': can_be_paid
|
||||
@@ -1780,25 +1799,24 @@ class OrderPayment(models.Model):
|
||||
))
|
||||
return
|
||||
|
||||
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum,
|
||||
generate_invoice)
|
||||
with transaction.atomic():
|
||||
self._mark_order_paid(count_waitinglist, send_mail, force, user, auth, mail_text, ignore_date, lock, payment_sum - refund_sum,
|
||||
generate_invoice)
|
||||
|
||||
def _mark_order_paid(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
|
||||
ignore_date=False, lock=True, payment_refund_sum=0, allow_generate_invoice=True):
|
||||
from pretix.base.services.invoices import (
|
||||
generate_invoice, invoice_qualified,
|
||||
)
|
||||
from pretix.base.services.locking import LOCK_TRUST_WINDOW
|
||||
|
||||
if (self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(seconds=LOCK_TIMEOUT * 2)) or not lock:
|
||||
if lock and self.order.status == Order.STATUS_PENDING and self.order.expires > now() + timedelta(seconds=LOCK_TRUST_WINDOW):
|
||||
# Performance optimization. In this case, there's really no reason to lock everything and an atomic
|
||||
# database transaction is more than enough.
|
||||
lockfn = NoLockManager
|
||||
else:
|
||||
lockfn = self.order.event.lock
|
||||
lock = False
|
||||
|
||||
with lockfn():
|
||||
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total,
|
||||
ignore_date=ignore_date)
|
||||
self._mark_paid_inner(force, count_waitinglist, user, auth, overpaid=payment_refund_sum > self.order.total,
|
||||
ignore_date=ignore_date, lock=lock)
|
||||
|
||||
invoice = None
|
||||
if invoice_qualified(self.order) and allow_generate_invoice:
|
||||
|
||||
@@ -435,28 +435,37 @@ class Voucher(LoggedModel):
|
||||
|
||||
@staticmethod
|
||||
def clean_quota_check(data, cnt, old_instance, event, quota, item, variation):
|
||||
from ..services.locking import lock_objects
|
||||
from ..services.quotas import QuotaAvailability
|
||||
|
||||
old_quotas = Voucher.clean_quota_get_ignored(old_instance)
|
||||
|
||||
if event.has_subevents and data.get('block_quota') and not data.get('subevent'):
|
||||
raise ValidationError(_('If you want this voucher to block quota, you need to select a specific date.'))
|
||||
|
||||
if quota:
|
||||
if quota in old_quotas:
|
||||
return
|
||||
else:
|
||||
avail = quota.availability(count_waitinglist=False)
|
||||
new_quotas = {quota}
|
||||
elif item and item.has_variations and not variation:
|
||||
raise ValidationError(_('You can only block quota if you specify a specific product variation. '
|
||||
'Otherwise it might be unclear which quotas to block.'))
|
||||
elif item and variation:
|
||||
avail = variation.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
||||
new_quotas = set(variation.quotas.filter(subevent=data.get('subevent')))
|
||||
elif item and not item.has_variations:
|
||||
avail = item.check_quotas(ignored_quotas=old_quotas, subevent=data.get('subevent'))
|
||||
new_quotas = set(item.quotas.filter(subevent=data.get('subevent')))
|
||||
else:
|
||||
raise ValidationError(_('You need to select a specific product or quota if this voucher should reserve '
|
||||
'tickets.'))
|
||||
|
||||
if avail[0] != Quota.AVAILABILITY_OK or (avail[1] is not None and avail[1] < cnt):
|
||||
if not (new_quotas - old_quotas):
|
||||
return
|
||||
|
||||
lock_objects([q for q in (new_quotas - old_quotas) if q.size is not None], shared_lock_objects=[event])
|
||||
|
||||
qa = QuotaAvailability(count_waitinglist=False)
|
||||
qa.queue(*(new_quotas - old_quotas))
|
||||
qa.compute()
|
||||
|
||||
if any(r[0] != Quota.AVAILABILITY_OK or (r[1] is not None and r[1] < cnt) for r in qa.results.values()):
|
||||
raise ValidationError(_('You cannot create a voucher that blocks quota as the selected product or '
|
||||
'quota is currently sold out or completely reserved.'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user