Fix performance issues in download reminder (#6393)

* Fix performance issues in download reminder

* Update src/pretix/base/services/orders.py

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Fixes after review

* Fix check in loop

---------

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
This commit is contained in:
Raphael Michel
2026-08-10 16:13:54 +02:00
committed by GitHub
co-authored by Richard Schreiber
parent 5cf28f1b81
commit f25c233e91
2 changed files with 93 additions and 72 deletions
+92 -71
View File
@@ -48,12 +48,12 @@ from celery.exceptions import MaxRetriesExceededError
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import models, transaction
from django.db.models import ( from django.db.models import (
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum, Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Subquery,
Value, Sum, Value,
) )
from django.db.models.functions import Coalesce, Greatest from django.db.models.functions import Cast, Greatest
from django.db.transaction import get_connection from django.db.transaction import get_connection
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.functional import cached_property from django.utils.functional import cached_property
@@ -71,7 +71,7 @@ from pretix.base.models import (
Membership, Order, OrderPayment, OrderPosition, Quota, Seat, Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
SeatCategoryMapping, User, Voucher, SeatCategoryMapping, User, Voucher,
) )
from pretix.base.models.event import SubEvent from pretix.base.models.event import Event_SettingsStore, SubEvent
from pretix.base.models.orders import ( from pretix.base.models.orders import (
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund, BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
generate_secret, generate_secret,
@@ -1494,83 +1494,104 @@ def send_expiry_warnings(sender, **kwargs):
@scopes_disabled() @scopes_disabled()
def send_download_reminders(sender, **kwargs): def send_download_reminders(sender, **kwargs):
today = now().replace(hour=0, minute=0, second=0, microsecond=0) today = now().replace(hour=0, minute=0, second=0, microsecond=0)
qs = Order.objects.annotate(
first_date=Coalesce( events = Event.objects.filter(
Min('all_positions__subevent__date_from'), Q(has_subevents=False, date_from__gte=now()) |
F('event__date_from') (Q(has_subevents=True) & Q(Exists(
SubEvent.objects.filter(event_id=OuterRef('id'), date_from__gte=now())
)))
).annotate(
reminder_days=Subquery(
Event_SettingsStore.objects.filter(
object=OuterRef('id'),
key='mail_days_download_reminder'
).exclude(
value="None"
).annotate(
val=Cast(F("value"), output_field=models.IntegerField()),
).values("val")
) )
).filter( ).filter(
download_reminder_sent=False, reminder_days__isnull=False,
datetime__lte=now() - timedelta(hours=2), ).order_by()
first_date__gte=today,
).only(
'pk', 'event_id', 'sales_channel', 'datetime',
).order_by('event_id')
event_id = None
days = None
event = None
for o in qs: for event in events.iterator(chunk_size=10_000):
if o.event_id != event_id: qs = event.orders.filter(
days = o.event.settings.get('mail_days_download_reminder', as_type=int) download_reminder_sent=False,
event = o.event datetime__lte=now() - timedelta(hours=2),
event_id = o.event_id )
if days is None: if event.has_subevents:
continue qs = qs.annotate(
first_date=Min('all_positions__subevent__date_from')
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder: ).filter(
continue Q(first_date__gte=today)
)
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0) else:
if now() < reminder_date or o.datetime > reminder_date: event_reminder_date = (event.date_from - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
continue if now() < event_reminder_date:
with transaction.atomic():
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
if o.download_reminder_sent:
# Race condition
continue
positions = list(o.positions_with_tickets)
if not positions:
continue continue
if not o.ticket_download_available: qs = qs.only(
'pk', 'event_id', 'sales_channel', 'datetime',
).order_by()
for o in qs:
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
continue continue
if o.status != Order.STATUS_PAID: if event.has_subevents:
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending): reminder_date = ((o.first_date or event.date_from) - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
else:
reminder_date = event_reminder_date
if now() < reminder_date or o.datetime > reminder_date:
continue
with transaction.atomic():
o = Order.objects.select_for_update(of=OF_SELF).get(pk=o.pk)
if o.download_reminder_sent:
# Race condition
continue
positions = list(o.positions_with_tickets)
if not positions:
continue continue
with language(o.locale, o.event.settings.region): if not o.ticket_download_available:
o.download_reminder_sent = True continue
o.save(update_fields=['download_reminder_sent'])
email_template = event.settings.mail_text_download_reminder
email_subject = event.settings.mail_subject_download_reminder
email_context = get_email_context(event=event, order=o)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True
)
if event.settings.mail_send_download_reminder_attendee: if o.status != Order.STATUS_PAID:
for p in positions: if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending):
if p.subevent_id: continue
reminder_date = (p.subevent.date_from - timedelta(days=days)).replace(
hour=0, minute=0, second=0, microsecond=0 with language(o.locale, o.event.settings.region):
) o.download_reminder_sent = True
if now() < reminder_date: o.save(update_fields=['download_reminder_sent'])
continue email_template = event.settings.mail_text_download_reminder
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email: email_subject = event.settings.mail_subject_download_reminder
email_template = event.settings.mail_text_download_reminder_attendee email_context = get_email_context(event=event, order=o)
email_subject = event.settings.mail_subject_download_reminder_attendee o.send_mail(
email_context = get_email_context(event=event, order=o, position=p) email_subject, email_template, email_context,
o.send_mail( 'pretix.event.order.email.download_reminder_sent',
email_subject, email_template, email_context, attach_tickets=True
'pretix.event.order.email.download_reminder_sent', )
attach_tickets=True, position=p
) if event.settings.mail_send_download_reminder_attendee:
for p in positions:
if p.subevent_id:
reminder_date = (p.subevent.date_from - timedelta(days=event.reminder_days)).replace(
hour=0, minute=0, second=0, microsecond=0
)
if now() < reminder_date:
continue
if p.addon_to_id is None and p.attendee_email and p.attendee_email != o.email:
email_template = event.settings.mail_text_download_reminder_attendee
email_subject = event.settings.mail_subject_download_reminder_attendee
email_context = get_email_context(event=event, order=o, position=p)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True, position=p
)
def notify_user_changed_order(order, user=None, auth=None, invoices=[]): def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
+1 -1
View File
@@ -2930,7 +2930,7 @@ Your {event} team""")) # noqa: W291
}, },
'mail_days_download_reminder': { 'mail_days_download_reminder': {
'type': int, 'type': int,
'default': None 'default': None # when this default is changed, send_download_reminders needs to change
}, },
'mail_send_download_reminder_attendee': { 'mail_send_download_reminder_attendee': {
'type': bool, 'type': bool,