Always process tasks through celery (#245)

This commit is contained in:
Raphael Michel
2016-09-21 10:38:31 +02:00
committed by GitHub
parent d014a92cef
commit 1faacef9d4
22 changed files with 145 additions and 213 deletions

View File

@@ -2,7 +2,7 @@ from datetime import datetime, timedelta
from decimal import Decimal
from typing import List, Optional
from django.conf import settings
from celery.exceptions import MaxRetriesExceededError
from django.db.models import Q
from django.utils.translation import ugettext as _
@@ -10,6 +10,8 @@ from pretix.base.i18n import LazyLocaleException
from pretix.base.models import (
CartPosition, Event, EventLock, Item, ItemVariation, Quota, Voucher,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.celery import app
class CartError(LazyLocaleException):
@@ -205,7 +207,8 @@ def _add_items_to_cart(event: Event, items: List[dict], cart_id: str=None) -> No
raise CartError(err)
def add_items_to_cart(event: int, items: List[dict], cart_id: str=None) -> None:
@app.task(bind=True, max_retries=5, default_retry_delay=1)
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None) -> None:
"""
Adds a list of items to a user's cart.
:param event: The event ID in question
@@ -216,8 +219,11 @@ def add_items_to_cart(event: int, items: List[dict], cart_id: str=None) -> None:
"""
event = Event.objects.get(id=event)
try:
_add_items_to_cart(event, items, cart_id)
except EventLock.LockTimeoutException:
try:
_add_items_to_cart(event, items, cart_id)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
raise CartError(error_messages['busy'])
@@ -242,7 +248,8 @@ def _remove_items_from_cart(event: Event, items: List[dict], cart_id: str) -> No
cp.delete()
def remove_items_from_cart(event: int, items: List[dict], cart_id: str=None) -> None:
@app.task(bind=True, max_retries=5, default_retry_delay=1)
def remove_items_from_cart(self, event: int, items: List[dict], cart_id: str=None) -> None:
"""
Removes a list of items from a user's cart.
:param event: The event ID in question
@@ -251,35 +258,9 @@ def remove_items_from_cart(event: int, items: List[dict], cart_id: str=None) ->
"""
event = Event.objects.get(id=event)
try:
_remove_items_from_cart(event, items, cart_id)
except EventLock.LockTimeoutException:
try:
_remove_items_from_cart(event, items, cart_id)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
raise CartError(error_messages['busy'])
if settings.HAS_CELERY:
from pretix.celery import app
@app.task(bind=True, max_retries=5, default_retry_delay=1)
def add_items_to_cart_task(self, event: int, items: List[dict], cart_id: str):
event = Event.objects.get(id=event)
try:
try:
_add_items_to_cart(event, items, cart_id)
except EventLock.LockTimeoutException:
self.retry(exc=CartError(error_messages['busy']))
except CartError as e:
return e
@app.task(bind=True, max_retries=5, default_retry_delay=1)
def remove_items_from_cart_task(self, event: int, items: List[dict], cart_id: str):
event = Event.objects.get(id=event)
try:
try:
_remove_items_from_cart(event, items, cart_id)
except EventLock.LockTimeoutException:
self.retry(exc=CartError(error_messages['busy']))
except CartError as e:
return e
add_items_to_cart.task = add_items_to_cart_task
remove_items_from_cart.task = remove_items_from_cart_task

View File

@@ -5,8 +5,10 @@ from django.core.files.base import ContentFile
from pretix.base.models import CachedFile, Event, cachedfile_name
from pretix.base.signals import register_data_exporters
from pretix.celery import app
@app.task()
def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
event = Event.objects.get(id=event)
file = CachedFile.objects.get(id=fileid)
@@ -17,12 +19,3 @@ def export(event: str, fileid: str, provider: str, form_data: Dict[str, Any]) ->
file.filename, file.type, data = ex.render(form_data)
file.file.save(cachedfile_name(file, file.filename), ContentFile(data))
file.save()
if settings.HAS_CELERY:
from pretix.celery import app
export_task = app.task(export)
def export(*args, **kwargs):
export_task.apply_async(args=args, kwargs=kwargs)

View File

@@ -24,6 +24,7 @@ from reportlab.platypus import (
from pretix.base.i18n import LazyI18nString, language
from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order
from pretix.base.signals import register_payment_providers
from pretix.celery import app
@transaction.atomic
@@ -374,7 +375,8 @@ def _invoice_generate_german(invoice, f):
return doc
def invoice_pdf(invoice: int):
@app.task
def invoice_pdf_task(invoice: int):
i = Invoice.objects.get(pk=invoice)
with language(i.locale):
with tempfile.NamedTemporaryFile(suffix=".pdf") as f:
@@ -391,13 +393,8 @@ def invoice_qualified(order: Order):
return True
if settings.HAS_CELERY:
from pretix.celery import app
invoice_pdf_task = app.task(invoice_pdf)
def invoice_pdf(*args, **kwargs):
# We introduce a 2 second delay, because otherwise we run into conditions where
# the task worker tries to generate the PDF even before our database transaction
# was committed and therefore fails to find the invoice object.
invoice_pdf_task.apply_async(args=args, kwargs=kwargs, countdown=2)
def invoice_pdf(*args, **kwargs):
# We introduce a 2 second delay, because otherwise we run into conditions where
# the task worker tries to generate the PDF even before our database transaction
# was committed and therefore fails to find the invoice object.
invoice_pdf_task.apply_async(args=args, kwargs=kwargs, countdown=2)

View File

@@ -27,13 +27,21 @@ class LockManager:
return False
class LockTimeoutException(Exception):
pass
class LockReleaseException(Exception):
pass
def lock_event(event):
"""
Issue a lock on this event so nobody can book tickets for this event until
you release the lock. Will retry 5 times on failure.
:raises EventLock.LockTimeoutException: if the event is locked every time we try
to obtain the lock
:raises LockTimeoutException: if the event is locked every time we try
to obtain the lock
"""
if hasattr(event, '_lock') and event._lock:
return True
@@ -50,10 +58,10 @@ def release_event(event):
the lock will only be released if it was issued in _this_ python
representation of the database object.
:raises EventLock.LockReleaseException: if we do not own the lock
:raises LockReleaseException: if we do not own the lock
"""
if not hasattr(event, '_lock') or not event._lock:
raise EventLock.LockReleaseException('Lock is not owned by this thread')
raise LockReleaseException('Lock is not owned by this thread')
if settings.HAS_REDIS:
return release_event_redis(event)
else:
@@ -70,26 +78,26 @@ def lock_event_db(event):
event._lock = l
return True
elif l.date < now() - timedelta(seconds=LOCK_TIMEOUT):
newtoken = uuid.uuid4()
newtoken = str(uuid.uuid4())
updated = EventLock.objects.filter(event=event.id, token=l.token).update(date=dt, token=newtoken)
if updated:
l.token = newtoken
event._lock = l
return True
time.sleep(2 ** i / 100)
raise EventLock.LockTimeoutException()
raise LockTimeoutException()
@transaction.atomic
def release_event_db(event):
if not hasattr(event, '_lock') or not event._lock:
raise EventLock.LockReleaseException('Lock is not owned by this thread')
raise LockReleaseException('Lock is not owned by this thread')
try:
lock = EventLock.objects.get(event=event.id, token=event._lock.token)
lock.delete()
event._lock = None
except EventLock.DoesNotExist:
raise EventLock.LockReleaseException('Lock is no longer owned by this thread')
raise LockReleaseException('Lock is no longer owned by this thread')
def redis_lock_from_event(event):
@@ -113,9 +121,9 @@ def lock_event_redis(event):
return True
except RedisError:
logger.exception('Error locking an event')
raise EventLock.LockTimeoutException()
raise LockTimeoutException()
time.sleep(2 ** i / 100)
raise EventLock.LockTimeoutException()
raise LockTimeoutException()
def release_event_redis(event):
@@ -126,5 +134,5 @@ def release_event_redis(event):
lock.release()
except RedisError:
logger.exception('Error releasing an event lock')
raise EventLock.LockTimeoutException()
raise LockTimeoutException()
event._lock = None

View File

@@ -8,6 +8,7 @@ from django.utils.translation import ugettext as _
from pretix.base.i18n import LazyI18nString, language
from pretix.base.models import Event, Order
from pretix.celery import app
from pretix.multidomain.urlreverse import build_absolute_uri
logger = logging.getLogger('pretix.base.mail')
@@ -90,7 +91,8 @@ def mail(email: str, subject: str, template: str,
return mail_send([email], subject, body, sender, event.id if event else None, headers)
def mail_send(to: str, subject: str, body: str, sender: str, event: int=None, headers: dict=None) -> bool:
@app.task
def mail_send_task(to: str, subject: str, body: str, sender: str, event: int=None, headers: dict=None) -> bool:
email = EmailMessage(subject, body, sender, to=to, headers=headers)
if event:
event = Event.objects.get(id=event)
@@ -105,10 +107,5 @@ def mail_send(to: str, subject: str, body: str, sender: str, event: int=None, he
raise SendMailException('Failed to send an email to {}.'.format(to))
if settings.HAS_CELERY and settings.EMAIL_BACKEND != 'django.core.mail.outbox':
from pretix.celery import app
mail_send_task = app.task(mail_send)
def mail_send(*args, **kwargs):
mail_send_task.apply_async(args=args, kwargs=kwargs)
def mail_send(*args, **kwargs):
mail_send_task.apply_async(args=args, kwargs=kwargs)

View File

@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
from decimal import Decimal
from typing import List, Optional
from django.conf import settings
from celery.exceptions import MaxRetriesExceededError
from django.db import transaction
from django.dispatch import receiver
from django.utils.formats import date_format
@@ -23,10 +23,12 @@ from pretix.base.payment import BasePaymentProvider
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_qualified,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import SendMailException, mail
from pretix.base.signals import (
order_paid, order_placed, periodic_task, register_payment_providers,
)
from pretix.celery import app
from pretix.multidomain.urlreverse import build_absolute_uri
error_messages = {
@@ -137,7 +139,7 @@ def mark_order_refunded(order, user=None):
@transaction.atomic
def cancel_order(order, user=None):
def _cancel_order(order, user=None):
"""
Mark this order as canceled
:param order: The order to change
@@ -348,16 +350,6 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
return order.id
def perform_order(event: str, payment_provider: str, positions: List[str],
email: str=None, locale: str=None, address: int=None):
try:
return _perform_order(event, payment_provider, positions, email, locale, address)
except EventLock.LockTimeoutException:
# Is raised when there are too many threads asking for event locks and we were
# unable to get one
raise OrderError(error_messages['busy'])
@receiver(signal=periodic_task)
def expire_orders(sender, **kwargs):
eventcache = {}
@@ -571,29 +563,24 @@ class OrderChangeManager:
raise OrderError(error_messages['internal'])
if settings.HAS_CELERY:
from pretix.celery import app
@app.task(bind=True, max_retries=5, default_retry_delay=1)
def perform_order_task(self, event: str, payment_provider: str, positions: List[str],
email: str=None, locale: str=None, address: int=None):
@app.task(bind=True, max_retries=5, default_retry_delay=1)
def perform_order(self, event: str, payment_provider: str, positions: List[str],
email: str=None, locale: str=None, address: int=None):
try:
try:
try:
return _perform_order(event, payment_provider, positions, email, locale, address)
except EventLock.LockTimeoutException:
self.retry(exc=OrderError(error_messages['busy']))
except OrderError as e:
return e
return _perform_order(event, payment_provider, positions, email, locale, address)
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):
return OrderError(error_messages['busy'])
@app.task(bind=True, max_retries=5, default_retry_delay=1)
def cancel_order_task(self, order: int, user: int=None):
@app.task(bind=True, max_retries=5, default_retry_delay=1)
def cancel_order(self, order: int, user: int=None):
try:
try:
try:
return cancel_order(order, user)
except EventLock.LockTimeoutException:
self.retry(exc=OrderError(error_messages['busy']))
except OrderError as e:
return e
perform_order.task = perform_order_task
cancel_order.task = cancel_order_task
return _cancel_order(order, user)
except LockTimeoutException:
self.retry(exc=OrderError(error_messages['busy']))
except (MaxRetriesExceededError, LockTimeoutException):
return OrderError(error_messages['busy'])

View File

@@ -1,13 +1,14 @@
from datetime import timedelta
from django.conf import settings
from django.core.files.base import ContentFile
from django.utils.timezone import now
from pretix.base.models import CachedFile, CachedTicket, Order, cachedfile_name
from pretix.base.signals import register_ticket_outputs
from pretix.celery import app
@app.task
def generate(order: str, provider: str):
order = Order.objects.select_related('event').get(id=order)
ct = CachedTicket.objects.get_or_create(order=order, provider=provider)[0]
@@ -26,12 +27,3 @@ def generate(order: str, provider: str):
ct.cachedfile.filename, ct.cachedfile.type, data = prov.generate(order)
ct.cachedfile.file.save(cachedfile_name(ct.cachedfile, ct.cachedfile.filename), ContentFile(data))
ct.cachedfile.save()
if settings.HAS_CELERY:
from pretix.celery import app
generate_task = app.task(generate)
def generate(*args, **kwargs):
generate_task.apply_async(args=args, kwargs=kwargs)