Fix performance and logic issues in auto-exit-all

This commit is contained in:
Raphael Michel
2023-01-25 09:50:36 +01:00
parent f81b7bcf53
commit 9eb2d43016
2 changed files with 66 additions and 58 deletions

View File

@@ -35,10 +35,11 @@ from datetime import timedelta
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import connection, models from django.db import models
from django.db.models import ( from django.db.models import (
Count, Exists, F, Max, OuterRef, Q, Subquery, Value, Window, Count, Exists, F, Max, OuterRef, Q, Subquery, Value, Window,
) )
from django.db.models.expressions import RawSQL
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, pgettext_lazy from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
@@ -98,15 +99,18 @@ class CheckinList(LoggedModel):
class Meta: class Meta:
ordering = ('subevent__date_from', 'name') ordering = ('subevent__date_from', 'name')
@property def positions_query(self, ignore_status=False):
def positions(self):
from . import Order, OrderPosition from . import Order, OrderPosition
qs = OrderPosition.objects.filter( qs = OrderPosition.all.filter(
order__event=self.event, order__event=self.event,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [
Order.STATUS_PAID],
) )
if not ignore_status:
qs = qs.filter(
canceled=False,
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID],
)
if self.subevent_id: if self.subevent_id:
qs = qs.filter(subevent_id=self.subevent_id) qs = qs.filter(subevent_id=self.subevent_id)
if not self.all_products: if not self.all_products:
@@ -114,10 +118,22 @@ class CheckinList(LoggedModel):
return qs return qs
@property @property
def positions_inside(self): def positions(self):
return self.positions.annotate( return self.positions_query(ignore_status=True)
@scopes_disabled()
def positions_inside_query(self, ignore_status=False, at_time=None):
if at_time is None:
c_q = []
else:
c_q = [Q(datetime__lt=at_time)]
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
# Use a simple approach that works on all databases
qs = self.positions_query(ignore_status=ignore_status).annotate(
last_entry=Subquery( last_entry=Subquery(
Checkin.objects.filter( Checkin.objects.filter(
*c_q,
position_id=OuterRef('pk'), position_id=OuterRef('pk'),
list_id=self.pk, list_id=self.pk,
type=Checkin.TYPE_ENTRY, type=Checkin.TYPE_ENTRY,
@@ -127,6 +143,7 @@ class CheckinList(LoggedModel):
), ),
last_exit=Subquery( last_exit=Subquery(
Checkin.objects.filter( Checkin.objects.filter(
*c_q,
position_id=OuterRef('pk'), position_id=OuterRef('pk'),
list_id=self.pk, list_id=self.pk,
type=Checkin.TYPE_EXIT, type=Checkin.TYPE_EXIT,
@@ -140,12 +157,7 @@ class CheckinList(LoggedModel):
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry')) Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
) )
) )
return qs
@property
def inside_count(self):
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
# Use the simple query that works on all databases
return self.positions_inside.count()
# Use the PostgreSQL-specific query using Window functions, which is a lot faster. # Use the PostgreSQL-specific query using Window functions, which is a lot faster.
# On a real-world example with ~100k tickets, of which ~17k are checked in, we observed # On a real-world example with ~100k tickets, of which ~17k are checked in, we observed
@@ -157,7 +169,7 @@ class CheckinList(LoggedModel):
# dedupliate by position and count it up. # dedupliate by position and count it up.
cl = self cl = self
base_q, base_params = ( base_q, base_params = (
Checkin.all.filter(successful=True, position__in=cl.positions, list=cl) Checkin.all.filter(*c_q, successful=True, list=cl)
.annotate( .annotate(
cnt_exists_after=Window( cnt_exists_after=Window(
expression=Count("position_id", filter=Q(type=Value("exit"))), expression=Count("position_id", filter=Q(type=Value("exit"))),
@@ -171,26 +183,25 @@ class CheckinList(LoggedModel):
.values("position_id", "type", "datetime", "cnt_exists_after") .values("position_id", "type", "datetime", "cnt_exists_after")
.query.sql_with_params() .query.sql_with_params()
) )
return self.positions.filter(
with connection.cursor() as cursor: pk__in=RawSQL(
cursor.execute(
f""" f"""
SELECT COUNT(*) FROM ( SELECT "position_id"
SELECT COUNT("position_id")
FROM ({str(base_q)}) s FROM ({str(base_q)}) s
WHERE "type" = %s AND "cnt_exists_after" = 0 WHERE "type" = %s AND "cnt_exists_after" = 0
GROUP BY "position_id" GROUP BY "position_id"
) a;
""", """,
[ [*base_params, Checkin.TYPE_ENTRY]
*base_params,
"entry",
],
) )
rows = cursor.fetchall() )
if rows:
return rows[0][0] @property
return 0 def positions_inside(self):
return self.positions_inside_query(None)
@property
def inside_count(self):
return self.positions_inside_query(None).count()
@property @property
@scopes_disabled() @scopes_disabled()

View File

@@ -842,10 +842,7 @@ def process_exit_all(sender, **kwargs):
exit_all_at__isnull=False exit_all_at__isnull=False
).select_related('event', 'event__organizer') ).select_related('event', 'event__organizer')
for cl in qs: for cl in qs:
positions = cl.positions_inside.filter( positions = cl.positions_inside_query(ignore_status=True, at_time=cl.exit_all_at)
Q(last_exit__isnull=True) | Q(last_exit__lte=cl.exit_all_at),
last_entry__lte=cl.exit_all_at,
)
for p in positions: for p in positions:
with scope(organizer=cl.event.organizer): with scope(organizer=cl.event.organizer):
ci = Checkin.objects.create( ci = Checkin.objects.create(