Fix performance issues in download reminder

This commit is contained in:
Raphael Michel
2026-07-17 10:25:09 +02:00
parent 2660dfe1bc
commit 971e925bfa
2 changed files with 83 additions and 70 deletions
+82 -69
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, Case, Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet,
Value, Subquery, Sum, Value, When,
) )
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,
@@ -1487,83 +1487,96 @@ 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'
).annotate(
val=Case(
When(value="None", then=None),
default=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: event_reminder_date = (event.date_from - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
days = o.event.settings.get('mail_days_download_reminder', as_type=int) if not event.has_subevents and now() < event_reminder_date:
event = o.event
event_id = o.event_id
if days is None:
continue continue
if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder: qs = event.orders.annotate(
continue first_date=Min('all_positions__subevent__date_from')
).filter(
Q(first_date__isnull=True) | Q(first_date__gte=today),
download_reminder_sent=False,
datetime__lte=now() - timedelta(hours=2),
).only(
'pk', 'event_id', 'sales_channel', 'datetime',
).order_by()
reminder_date = (o.first_date - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0) for o in qs:
if now() < reminder_date or o.datetime > reminder_date: if o.sales_channel.identifier not in event.settings.mail_sales_channel_download_reminder:
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
if not o.ticket_download_available: reminder_date = ((o.first_date or event.date_from) - timedelta(days=event.reminder_days)).replace(hour=0, minute=0, second=0, microsecond=0)
if now() < reminder_date or o.datetime > reminder_date:
continue continue
if o.status != Order.STATUS_PAID: with transaction.atomic():
if o.status != Order.STATUS_PENDING or o.require_approval or (not o.valid_if_pending and not o.event.settings.ticket_download_pending): 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,