mirror of
https://github.com/pretix/pretix.git
synced 2025-12-05 21:32:28 +00:00
Compare commits
1 Commits
widget-coo
...
quota-lock
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c07460cd4 |
@@ -31,6 +31,8 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from collections import Counter, defaultdict, namedtuple
|
||||
@@ -80,6 +82,8 @@ from pretix.presale.signals import (
|
||||
)
|
||||
from pretix.testutils.middleware import debugflags_var
|
||||
|
||||
quota_logger = logging.getLogger("pretix_quota")
|
||||
|
||||
|
||||
class CartError(Exception):
|
||||
def __init__(self, *args):
|
||||
@@ -1123,6 +1127,11 @@ class CartManager:
|
||||
)
|
||||
vouchers_ok = self._get_voucher_availability()
|
||||
quotas_ok = _get_quota_availability(self._quota_diff, self.real_now_dt)
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "CartManager._perform_operations",
|
||||
"quota_diff": {q.name: d for q, d in self._quota_diff.items()},
|
||||
"quotas_ok": {q.name: d for q, d in quotas_ok.items()},
|
||||
}))
|
||||
err = None
|
||||
new_cart_positions = []
|
||||
deleted_positions = set()
|
||||
@@ -1408,6 +1417,16 @@ class CartManager:
|
||||
return diff
|
||||
|
||||
def commit(self):
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "CartManager.commit start",
|
||||
}))
|
||||
for op in self._operations:
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "CartManager.commit operation",
|
||||
"operation_type": str(type(op)),
|
||||
"count": getattr(op, "count", 1) if isinstance(op, (self.AddOperation, self.ExtendOperation)) else None,
|
||||
"repr": repr(op),
|
||||
}))
|
||||
self._check_presale_dates()
|
||||
self._check_max_cart_size()
|
||||
self._calculate_expiry()
|
||||
@@ -1424,6 +1443,16 @@ class CartManager:
|
||||
|
||||
if err:
|
||||
raise CartError(err)
|
||||
for op in self._operations:
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "CartManager.commit operation done",
|
||||
"operation_type": str(type(op)),
|
||||
"count": getattr(op, "count", 1) if isinstance(op, (self.AddOperation, self.ExtendOperation)) else None,
|
||||
"repr": repr(op),
|
||||
}))
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "CartManager.commit done",
|
||||
}))
|
||||
|
||||
|
||||
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
|
||||
@@ -1516,6 +1545,11 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
:param cart_id: Session ID of a guest
|
||||
:raises CartError: On any error that occurred
|
||||
"""
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "add_items_to_cart start",
|
||||
"cart_id": cart_id,
|
||||
"items": items,
|
||||
}))
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
ia = False
|
||||
if invoice_address:
|
||||
@@ -1540,6 +1574,10 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
raise CartError(error_messages['busy'])
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "add_items_to_cart end",
|
||||
"cart_id": cart_id,
|
||||
}))
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
@@ -1549,6 +1587,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
|
||||
:param voucher: A voucher code
|
||||
:param cart_id: The cart ID of the cart to modify
|
||||
"""
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "apply_voucher start",
|
||||
"cart_id": cart_id,
|
||||
}))
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
@@ -1563,6 +1605,10 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
raise CartError(error_messages['busy'])
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "apply_voucher end",
|
||||
"cart_id": cart_id,
|
||||
}))
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
@@ -1573,6 +1619,11 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
|
||||
:param position: A cart position ID
|
||||
:param cart_id: The cart ID of the cart to modify
|
||||
"""
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "remove_cart_position start",
|
||||
"cart_id": cart_id,
|
||||
"position": position,
|
||||
}))
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
@@ -1587,6 +1638,11 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
raise CartError(error_messages['busy'])
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "remove_cart_position end",
|
||||
"cart_id": cart_id,
|
||||
"position": position,
|
||||
}))
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
@@ -1596,6 +1652,10 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
||||
:param event: The event ID in question
|
||||
:param cart_id: The cart ID of the cart to modify
|
||||
"""
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "clear_cart start",
|
||||
"cart_id": cart_id,
|
||||
}))
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
@@ -1610,6 +1670,10 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
raise CartError(error_messages['busy'])
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "clear_cart end",
|
||||
"cart_id": cart_id,
|
||||
}))
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
|
||||
@@ -19,18 +19,19 @@
|
||||
# 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 json
|
||||
import logging
|
||||
from itertools import groupby
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import DatabaseError, connection
|
||||
from django.db import DatabaseError, connection, transaction
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.models import Event, Membership, Quota, Seat, Voucher
|
||||
from pretix.testutils.middleware import debugflags_var
|
||||
|
||||
logger = logging.getLogger('pretix.base.locking')
|
||||
quota_logger = logging.getLogger("pretix_quota")
|
||||
|
||||
# A lock acquisition is aborted if it takes longer than LOCK_ACQUISITION_TIMEOUT to prevent connection starvation
|
||||
LOCK_ACQUISITION_TIMEOUT = 3
|
||||
@@ -97,6 +98,10 @@ def lock_objects(objects, *, shared_lock_objects=None, replace_exclusive_with_sh
|
||||
"You cannot create locks outside of an transaction"
|
||||
)
|
||||
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "lock_objects",
|
||||
"objects": [{"type": str(type(o)), "pk": o.pk, "str": str(o)} for o in objects],
|
||||
}))
|
||||
if 'postgresql' in settings.DATABASES['default']['ENGINE']:
|
||||
shared_keys = set(pg_lock_key(obj) for obj in shared_lock_objects) if shared_lock_objects else set()
|
||||
exclusive_keys = set(pg_lock_key(obj) for obj in objects)
|
||||
@@ -120,6 +125,14 @@ def lock_objects(objects, *, shared_lock_objects=None, replace_exclusive_with_sh
|
||||
else:
|
||||
for model, instances in groupby(objects, key=lambda o: type(o)):
|
||||
model.objects.select_for_update().filter(pk__in=[o.pk for o in instances])
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "lock_objects acquired",
|
||||
"objects": [{"type": str(type(o)), "pk": o.pk, "str": str(o)} for o in objects],
|
||||
}))
|
||||
transaction.on_commit(lambda: quota_logger.debug(json.dumps({
|
||||
"op": "lock_objects released",
|
||||
"reason": "transaction.on_commit",
|
||||
})))
|
||||
|
||||
|
||||
class NoLockManager:
|
||||
|
||||
@@ -203,6 +203,7 @@ error_messages = {
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
quota_logger = logging.getLogger("pretix_quota")
|
||||
|
||||
|
||||
def mark_order_paid(*args, **kwargs):
|
||||
@@ -694,6 +695,10 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
[op.seat for op in sorted_positions if op.seat],
|
||||
shared_lock_objects=[event]
|
||||
)
|
||||
else:
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "_check_items lock skipped due to trust window",
|
||||
}))
|
||||
|
||||
# Check availability
|
||||
for i, cp in enumerate(sorted_positions):
|
||||
@@ -811,6 +816,10 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
cp.voucher.allow_ignore_quota or (cp.voucher.block_quota and cp.voucher.quota is None)
|
||||
)
|
||||
)
|
||||
if ignore_all_quotas:
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "quotas skipped due to trust window or voucher",
|
||||
}))
|
||||
|
||||
if not ignore_all_quotas:
|
||||
for quota in quotas:
|
||||
@@ -827,7 +836,15 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
|
||||
|
||||
if not quota_ok:
|
||||
# Sorry, can't let you keep that!
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "_check_items quota not ok",
|
||||
}))
|
||||
delete(cp)
|
||||
else:
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "_check_items ok",
|
||||
"q_avail_after": [{"id": q.pk, "str": q.name, "avail": v} for q, v in q_avail.items()]
|
||||
}))
|
||||
|
||||
for voucher, cnt in v_usages.items():
|
||||
if 0 < cnt < voucher.min_usages_remaining:
|
||||
@@ -1135,6 +1152,18 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
||||
).filter(
|
||||
id__in=position_ids, event=event
|
||||
)
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "perform_order positions",
|
||||
"positions": [
|
||||
{
|
||||
"item_id": p.item_id,
|
||||
"variation_id": p.variation_id,
|
||||
"id": p.pk,
|
||||
"addon_to": p.addon_to_id,
|
||||
"expires": p.expires.isoformat(),
|
||||
} for p in positions
|
||||
]
|
||||
}))
|
||||
|
||||
if shown_total is not None and Decimal(shown_total) > Decimal("0.00") and event.currency == "XXX":
|
||||
raise OrderError(error_messages['currency_XXX'])
|
||||
@@ -2806,6 +2835,16 @@ class OrderChangeManager:
|
||||
# Do nothing
|
||||
return
|
||||
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "OrderChangeManager.commit start",
|
||||
}))
|
||||
for op in self._operations:
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "OrderChangeManager.commit operation",
|
||||
"operation_type": str(type(op)),
|
||||
"repr": repr(op),
|
||||
}))
|
||||
|
||||
# Clear prefetched objects cache of order. We're going to modify the positions and fees and we have no guarantee
|
||||
# that every operation tuple points to a position/fee instance that has been fetched from the same object cache,
|
||||
# so it's dangerous to keep the cache around.
|
||||
@@ -2854,6 +2893,15 @@ class OrderChangeManager:
|
||||
)
|
||||
|
||||
order_changed.send(self.order.event, order=self.order)
|
||||
for op in self._operations:
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "OrderChangeManager.commit operation done",
|
||||
"operation_type": str(type(op)),
|
||||
"repr": repr(op),
|
||||
}))
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "OrderChangeManager.commit end",
|
||||
}))
|
||||
|
||||
def _clear_tickets_cache(self):
|
||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.event.pk,
|
||||
@@ -2877,11 +2925,18 @@ def perform_order(self, event: Event, payments: List[dict], positions: List[str]
|
||||
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
||||
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None,
|
||||
api_meta: dict=None):
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "perform_order start",
|
||||
}))
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
try:
|
||||
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
||||
sales_channel, shown_total, customer, api_meta)
|
||||
r = _perform_order(event, payments, positions, email, locale, address, meta_info,
|
||||
sales_channel, shown_total, customer, api_meta)
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "perform_order end",
|
||||
}))
|
||||
return r
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
@@ -3012,6 +3067,9 @@ def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
||||
device=None, cancellation_fee=None, try_auto_refund=False, refund_as_giftcard=False,
|
||||
email_comment=None, refund_comment=None, cancel_invoice=True):
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "cancel_order start",
|
||||
}))
|
||||
try:
|
||||
try:
|
||||
ret = _cancel_order(order, user, send_mail, api_token, device, oauth_application,
|
||||
@@ -3019,6 +3077,9 @@ def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_tok
|
||||
if try_auto_refund:
|
||||
_try_auto_refund(order, refund_as_giftcard=refund_as_giftcard,
|
||||
comment=refund_comment)
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "cancel_order end",
|
||||
}))
|
||||
return ret
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# 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 json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter, defaultdict
|
||||
@@ -40,6 +42,8 @@ from pretix.base.models import (
|
||||
|
||||
from ..signals import quota_availability
|
||||
|
||||
quota_logger = logging.getLogger("pretix_quota")
|
||||
|
||||
|
||||
class QuotaAvailability:
|
||||
"""
|
||||
@@ -146,6 +150,10 @@ class QuotaAvailability:
|
||||
self.results[q] = int(data[0]), None
|
||||
else:
|
||||
self.results[q] = int(data[0]), int(data[1])
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "quota_from_cache",
|
||||
"objects": [{"pk": q.pk, "str": str(q), "result": r} for q, r in self.results.items()],
|
||||
}))
|
||||
|
||||
if not quota_ids_set:
|
||||
return
|
||||
@@ -161,6 +169,11 @@ class QuotaAvailability:
|
||||
count_waitinglist=self.count_waitinglist):
|
||||
self.results[q] = resp
|
||||
|
||||
quota_logger.debug(json.dumps({
|
||||
"op": "quota_results",
|
||||
"objects": [{"pk": q.pk, "str": str(q), "result": r} for q, r in self.results.items()],
|
||||
}))
|
||||
|
||||
self._close(quotas)
|
||||
self._write_cache(quotas, now_dt)
|
||||
|
||||
|
||||
@@ -556,6 +556,13 @@ LOGGING = {
|
||||
else '%(levelname)s %(asctime)s %(name)s %(module)s %(message)s'
|
||||
)
|
||||
},
|
||||
'quota_default': {
|
||||
'format': (
|
||||
'%(levelname)s %(asctime)s %(thread)d %(threadName)s RequestId=%(request_id)s %(name)s %(module)s %(message)s'
|
||||
if REQUEST_ID_HEADER
|
||||
else '%(levelname)s %(asctime)s %(thread)d %(threadName)s %(name)s %(module)s %(message)s'
|
||||
)
|
||||
},
|
||||
},
|
||||
'filters': {
|
||||
'require_admin_enabled': {
|
||||
@@ -586,6 +593,13 @@ LOGGING = {
|
||||
'formatter': 'default',
|
||||
'filters': ['request_id'],
|
||||
},
|
||||
'quota_file': {
|
||||
'level': logging.DEBUG,
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': os.path.join(LOG_DIR, 'quota.log'),
|
||||
'formatter': 'quota_default',
|
||||
'filters': ['request_id'],
|
||||
},
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'class': 'django.utils.log.AdminEmailHandler',
|
||||
@@ -601,6 +615,11 @@ LOGGING = {
|
||||
'level': loglevel,
|
||||
'propagate': True,
|
||||
},
|
||||
'pretix_quota': {
|
||||
'handlers': ['quota_file'],
|
||||
'level': logging.DEBUG,
|
||||
'propagate': True,
|
||||
},
|
||||
'django.request': {
|
||||
'handlers': ['file', 'console', 'mail_admins'],
|
||||
'level': loglevel,
|
||||
|
||||
Reference in New Issue
Block a user