diff --git a/doc/development/implementation/background.rst b/doc/development/implementation/background.rst index 2fdac0faa..bbdadb63b 100644 --- a/doc/development/implementation/background.rst +++ b/doc/development/implementation/background.rst @@ -5,33 +5,27 @@ pretix provides the ability to run all longer-running tasks like generating tick in a background thread instead of the web server process. We use the well-established `Celery`_ project to implement this. However, as celery requires running a task queue like RabbitMQ and a result storage such as Redis to work efficiently, we don't like to *depend* on celery being available to make small-scale installations -of pretix more straightforward. For this reason, the "background" in "background task" is always optional. - -The Django settings variable ``settings.HAS_CELERY`` provides information on whether celery is configured -in the current installation. +of pretix more straightforward. For this reason, the "background" in "background task" is always optional. If +no celery broker is configured, celery will be configured to run tasks synchronously. Implementing a task ------------------- -A common pattern for implementing "optionally-asynchronous" tasks can be seen a lot in ``pretix.base.services`` +A common pattern for implementing asynchronous tasks can be seen a lot in ``pretix.base.services`` and looks like this:: + from pretix.celery import app + + @app.task def my_task(argument1, argument2): # Important: All arguments and return values need to be serializable into JSON. # Do not use model instances, use their primary keys instead! pass # do your work here - if settings.HAS_CELERY: - # Transform this into a background task - from pretix.celery import app # Important: Do not import this unconditionally! + # Call the task like this: + # my_task.apply_async(args=(…,), kwargs={…}) - my_task_async = app.task(export) - - def my_task(*args, **kwargs): - my_task_async.apply_async(args=args, kwargs=kwargs) - -This explicit declaration method also allows you to place some custom retry logic etc. in the asynchronous version. Tasks in the request-response flow ---------------------------------- @@ -44,7 +38,7 @@ A usage example taken directly from the code is:: class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View): """ - A view that executes a task asynchronously. A POST request will kick of the + A view that executes a task asynchronously. A POST request will kick off the task into the background or run it in the foreground if celery is not installed. In the former case, subsequent GET calls can be used to determinine the current status of the task. diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 83f491bb1..ca3aad943 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -273,9 +273,3 @@ class EventLock(models.Model): event = models.CharField(max_length=36, primary_key=True) date = models.DateTimeField(auto_now=True) token = models.UUIDField(default=uuid.uuid4) - - class LockTimeoutException(Exception): - pass - - class LockReleaseException(Exception): - pass diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 57faba305..de93bf813 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -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 diff --git a/src/pretix/base/services/export.py b/src/pretix/base/services/export.py index 5aa2d2135..d2a6bf2e4 100644 --- a/src/pretix/base/services/export.py +++ b/src/pretix/base/services/export.py @@ -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) diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index 9099058f5..07580fa06 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -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) diff --git a/src/pretix/base/services/locking.py b/src/pretix/base/services/locking.py index 565df6ed2..a2ef2dd45 100644 --- a/src/pretix/base/services/locking.py +++ b/src/pretix/base/services/locking.py @@ -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 diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index f670dcb90..362050f52 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -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) diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 947b64521..e6bbde865 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -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']) diff --git a/src/pretix/base/services/tickets.py b/src/pretix/base/services/tickets.py index 422c16788..f3b4d0c48 100644 --- a/src/pretix/base/services/tickets.py +++ b/src/pretix/base/services/tickets.py @@ -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) diff --git a/src/pretix/base/views/async.py b/src/pretix/base/views/async.py index 9be9f684f..6c07bf983 100644 --- a/src/pretix/base/views/async.py +++ b/src/pretix/base/views/async.py @@ -1,11 +1,15 @@ import logging +import celery.exceptions +from celery.result import AsyncResult from django.conf import settings from django.contrib import messages from django.http import JsonResponse from django.shortcuts import redirect, render from django.utils.translation import ugettext as _ +from pretix.celery import app + logger = logging.getLogger('pretix.base.async') @@ -15,15 +19,22 @@ class AsyncAction: error_url = None def do(self, *args): - if settings.HAS_CELERY: - from pretix.celery import app + if not isinstance(self.task, app.Task): + raise TypeError('Method has no task attached') - if hasattr(self.task, 'task') and isinstance(self.task.task, app.Task): - return self._do_celery(args) - else: - raise TypeError('Method has no task attached') + res = self.task.apply_async(args=args) + + if 'ajax' in self.request.GET or 'ajax' in self.request.POST: + data = self._return_ajax_result(res) + data['check_url'] = self.get_check_url(res.id, True) + return JsonResponse(data) else: - return self._do_sync(args) + if res.ready(): + if res.successful(): + return self.success(res.info) + else: + return self.error(res.info) + return redirect(self.get_check_url(res.id, False)) def get_success_url(self, value): return self.success_url @@ -39,14 +50,13 @@ class AsyncAction: return self.get_result(request) return self.http_method_not_allowed(request) - def _return_celery_result(self, res, timeout=.5): - import celery.exceptions - + def _return_ajax_result(self, res, timeout=.5): if not res.ready(): try: res.get(timeout=timeout) except celery.exceptions.TimeoutError: pass + ready = res.ready() data = { 'async_id': res.id, @@ -57,7 +67,7 @@ class AsyncAction: smes = self.get_success_message(res.info) if smes: messages.success(self.request, smes) - # TODO: Do not store message if the ajax client stats that it will not redirect + # TODO: Do not store message if the ajax client states that it will not redirect # but handle the mssage itself data.update({ 'redirect': self.get_success_url(res.info), @@ -65,7 +75,7 @@ class AsyncAction: }) else: messages.error(self.request, self.get_error_message(res.info)) - # TODO: Do not store message if the ajax client stats that it will not redirect + # TODO: Do not store message if the ajax client states that it will not redirect # but handle the mssage itself data.update({ 'redirect': self.get_error_url(), @@ -74,11 +84,9 @@ class AsyncAction: return data def get_result(self, request): - from celery.result import AsyncResult - res = AsyncResult(request.GET.get('async_id')) if 'ajax' in self.request.GET: - return JsonResponse(self._return_celery_result(res, timeout=0.25)) + return JsonResponse(self._return_ajax_result(res, timeout=0.25)) else: if res.ready(): if res.successful(): @@ -87,23 +95,6 @@ class AsyncAction: return self.error(res.info) return render(request, 'pretixpresale/waiting.html') - def _do_celery(self, args): - res = self.task.task.apply_async(args=args) - if 'ajax' in self.request.GET or 'ajax' in self.request.POST: - data = self._return_celery_result(res) - data['check_url'] = self.get_check_url(res.id, True) - return JsonResponse(data) - else: - return redirect(self.get_check_url(res.id, False)) - - def _do_sync(self, args): - try: - rs = getattr(self.__class__, 'task')(*args) - return self.success(rs) - except Exception as e: - logger.exception('Error while executing task synchronously') - return self.error(e) - def success(self, value): smes = self.get_success_message(value) if smes: diff --git a/src/pretix/celery.py b/src/pretix/celery.py index 0fcaeaf45..58b28df29 100644 --- a/src/pretix/celery.py +++ b/src/pretix/celery.py @@ -1,12 +1,12 @@ import os +from celery import Celery + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pretix.settings") from django.conf import settings -if settings.HAS_CELERY: - from celery import Celery - app = Celery('pretix') +app = Celery('pretix') - app.config_from_object('django.conf:settings') - app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) +app.config_from_object('django.conf:settings') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 17896c37a..4167184e3 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -22,6 +22,7 @@ from pretix.base.services.invoices import ( generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified, regenerate_invoice, ) +from pretix.base.services.locking import LockTimeoutException from pretix.base.services.mail import SendMailException, mail from pretix.base.services.orders import ( OrderChangeManager, OrderError, cancel_order, mark_order_paid, @@ -433,7 +434,7 @@ class OrderDownload(OrderView): ct.cachedfile = cf ct.save() if not ct.cachedfile.file.name: - tickets.generate(self.order.id, self.output.identifier) + tickets.generate.apply_async(args=(self.order.id, self.output.identifier)) return redirect(reverse('cachedfile.download', kwargs={'id': ct.cachedfile.id})) @@ -465,7 +466,7 @@ class OrderExtend(OrderView): messages.success(self.request, _('The payment term has been changed.')) else: messages.error(self.request, is_available) - except EventLock.LockTimeoutException: + except LockTimeoutException: messages.error(self.request, _('We were not able to process the request completely as the ' 'server was too busy.')) return self._redirect_back() @@ -660,5 +661,6 @@ class ExportView(EventPermissionRequiredMixin, TemplateView): cf.date = now() cf.expires = now() + timedelta(days=3) cf.save() - export(self.request.event.id, str(cf.id), self.exporter.identifier, self.exporter.form.cleaned_data) + export.apply_async(args=(self.request.event.id, str(cf.id), self.exporter.identifier, + self.exporter.form.cleaned_data)) return redirect(reverse('cachedfile.download', kwargs={'id': str(cf.id)})) diff --git a/src/pretix/plugins/banktransfer/tasks.py b/src/pretix/plugins/banktransfer/tasks.py index 60cd739f6..fe7c621a5 100644 --- a/src/pretix/plugins/banktransfer/tasks.py +++ b/src/pretix/plugins/banktransfer/tasks.py @@ -11,6 +11,7 @@ from pretix.base.i18n import language from pretix.base.models import Event, Order, Quota from pretix.base.services.mail import SendMailException from pretix.base.services.orders import mark_order_paid +from pretix.celery import app from .models import BankImportJob, BankTransaction @@ -94,6 +95,7 @@ def _get_unknown_transactions(event: Event, job: BankImportJob, data: list): return transactions +@app.task def process_banktransfers(event: int, job: int, data: list) -> None: with language("en"): # We'll translate error messages at display time event = Event.objects.get(pk=event) @@ -127,12 +129,3 @@ def process_banktransfers(event: int, job: int, data: list) -> None: else: job.state = BankImportJob.STATE_COMPLETED job.save() - - -if settings.HAS_CELERY: - from pretix.celery import app - - process_task = app.task(process_banktransfers) - - def process_banktransfers(*args, **kwargs): - process_task.apply_async(args=args, kwargs=kwargs) diff --git a/src/pretix/plugins/banktransfer/views.py b/src/pretix/plugins/banktransfer/views.py index c88c88427..05d04afd1 100644 --- a/src/pretix/plugins/banktransfer/views.py +++ b/src/pretix/plugins/banktransfer/views.py @@ -311,7 +311,11 @@ class ImportView(EventPermissionRequiredMixin, ListView): def start_processing(self, parsed): job = BankImportJob.objects.create(event=self.request.event) - process_banktransfers(event=self.request.event.pk, job=job.pk, data=parsed) + process_banktransfers.apply_async(kwargs={ + 'event': self.request.event.pk, + 'job': job.pk, + 'data': parsed + }) return redirect(reverse('plugins:banktransfer:import.job', kwargs={ 'event': self.request.event.slug, 'organizer': self.request.event.organizer.slug, diff --git a/src/pretix/presale/style.py b/src/pretix/presale/style.py index e0bc12471..c4fa493e1 100644 --- a/src/pretix/presale/style.py +++ b/src/pretix/presale/style.py @@ -9,10 +9,12 @@ from django.core.files.base import ContentFile from django.core.files.storage import default_storage from pretix.base.models import Event +from pretix.celery import app logger = logging.getLogger('pretix.presale.style') +@app.task def regenerate_css(event_id: int): event = Event.objects.select_related('organizer').get(pk=event_id) sassdir = os.path.join(settings.STATIC_ROOT, 'pretixpresale/scss') @@ -36,12 +38,3 @@ def regenerate_css(event_id: int): newname = default_storage.save(fname, ContentFile(css)) event.settings.set('presale_css_file', newname) event.settings.set('presale_css_checksum', checksum) - - -if settings.HAS_CELERY: - from pretix.celery import app - - regenerate_css_task = app.task(regenerate_css) - - def regenerate_css(*args, **kwargs): - regenerate_css_task.apply_async(args=args, kwargs=kwargs) diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index ec558343c..4d0438478 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -500,7 +500,7 @@ class OrderDownload(EventViewMixin, OrderDetailMixin, View): cf.save() ct.cachedfile = cf ct.save() - generate(self.order.id, self.output.identifier) + generate.apply_async(args=(self.order.id, self.output.identifier)) return redirect(reverse('cachedfile.download', kwargs={'id': ct.cachedfile.id})) diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 2db44361e..446c683ce 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -132,6 +132,8 @@ if HAS_CELERY: BROKER_URL = config.get('celery', 'broker') CELERY_RESULT_BACKEND = config.get('celery', 'backend') CELERY_SEND_TASK_ERROR_EMAILS = bool(ADMINS) +else: + CELERY_ALWAYS_EAGER = True SESSION_COOKIE_DOMAIN = config.get('pretix', 'cookie_domain', fallback=None) diff --git a/src/requirements/celery.txt b/src/requirements/celery.txt deleted file mode 100644 index 8563e9a4d..000000000 --- a/src/requirements/celery.txt +++ /dev/null @@ -1,5 +0,0 @@ -# celery>=3.1,<3.2 -# until the following issue is fixed, we need our own celery version -# https://github.com/celery/celery/pull/3199 -git+https://github.com/pretix/celery.git@pretix#egg=celery - diff --git a/src/requirements/production.txt b/src/requirements/production.txt index a8518f014..150a65484 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -10,6 +10,10 @@ git+https://github.com/pretix/PyPDF2.git@pretix#egg=PyPDF2 easy-thumbnails>=2.2,<3 django-libsass libsass +# celery>=3.1,<3.2 +# until the following issue is fixed, we need our own celery version +# https://github.com/celery/celery/pull/3199 +git+https://github.com/pretix/celery.git@pretix#egg=celery # Deployment / static file compilation requirements BeautifulSoup4 diff --git a/src/tests/base/test_locking.py b/src/tests/base/test_locking.py index e4e2d3580..c98e99097 100644 --- a/src/tests/base/test_locking.py +++ b/src/tests/base/test_locking.py @@ -5,6 +5,9 @@ from django.utils.timezone import now from pretix.base.models import Event, EventLock, Organizer from pretix.base.services import locking +from pretix.base.services.locking import ( + LockReleaseException, LockTimeoutException, +) @pytest.fixture @@ -20,7 +23,7 @@ def event(): @pytest.mark.django_db def test_locking_exclusive(event): with event.lock(): - with pytest.raises(EventLock.LockTimeoutException): + with pytest.raises(LockTimeoutException): ev = Event.objects.get(id=event.id) with ev.lock(): pass @@ -41,10 +44,10 @@ def test_locking_different_events(event): def test_lock_timeout_steal(event): locking.LOCK_TIMEOUT = 1 locking.lock_event(event) - with pytest.raises(EventLock.LockTimeoutException): + with pytest.raises(LockTimeoutException): ev = Event.objects.get(id=event.id) locking.lock_event(ev) time.sleep(1.5) locking.lock_event(ev) - with pytest.raises(EventLock.LockReleaseException): + with pytest.raises(LockReleaseException): locking.release_event(event) diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py index afab0902e..2efda96b9 100644 --- a/src/tests/control/test_orders.py +++ b/src/tests/control/test_orders.py @@ -298,7 +298,7 @@ def test_order_download_disabled_provider(client, env): @pytest.mark.django_db def test_order_download_success(client, env, mocker): from pretix.base.services import tickets - mocker.patch('pretix.base.services.tickets.generate') + mocker.patch('pretix.base.services.tickets.generate.apply_async') o = Order.objects.get(id=env[2].id) o.status = Order.STATUS_PAID o.save() @@ -308,16 +308,16 @@ def test_order_download_success(client, env, mocker): client.login(email='dummy@dummy.dummy', password='dummy') response = client.get('/control/event/dummy/dummy/orders/FOO/download/testdummy') assert response.status_code == 302 - tickets.generate.assert_any_call(o.id, 'testdummy') + tickets.generate.apply_async.assert_any_call(args=(o.id, 'testdummy')) assert 'download' in response['Location'] dl = response['Location'] assert CachedTicket.objects.filter(order=o, provider='testdummy').exists() # test caching - tickets.generate.reset_mock() + tickets.generate.apply_async.reset_mock() response = client.get('/control/event/dummy/dummy/orders/FOO/download/testdummy') assert response.status_code == 302 - assert tickets.generate.assert_not_called() + assert tickets.generate.apply_async.assert_not_called() assert dl == response['Location'] diff --git a/src/tests/settings.py b/src/tests/settings.py index 9caa722d9..e126b28d4 100644 --- a/src/tests/settings.py +++ b/src/tests/settings.py @@ -15,3 +15,5 @@ EMAIL_BACKEND = 'django.core.mail.outbox' COMPRESS_ENABLED = COMPRESS_OFFLINE = False PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] + +CELERY_ALWAYS_EAGER = True