Run exporters in repeatable read by default (Z#23173095) (#5500)

* Run exporters in repeatable read by default (Z#23173095)

* Update src/pretix/helpers/database.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Rename parameter, add test

* Do not run during tests

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2025-10-06 10:38:19 +02:00
committed by GitHub
parent 42b1010c36
commit 85a9a3caa6
11 changed files with 115 additions and 8 deletions

View File

@@ -21,7 +21,8 @@
#
import contextlib
from django.core.exceptions import FieldDoesNotExist
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.db import connection, transaction
from django.db.models import (
Aggregate, Expression, F, Field, Lookup, OrderBy, Value,
@@ -62,6 +63,43 @@ def casual_reads():
yield
@contextlib.contextmanager
def repeatable_reads_transaction():
"""
pretix, and Django, operate in the transaction isolation level READ COMMITTED by default. This is not a strong level
of isolation, but we NEED to use it: Otherwise e.g. our quota logic breaks, because we need to be able to get the
*current* number of tickets sold at any time in a transaction, not the number of tickets sold *before* our transaction
started.
However, this isolation mode has drawbacks, for example during reporting. When a user retrieves a report from the
system, it should return numbers that are consistent with each other. However, if the report makes multiple SQL
queries in READ COMMITTED mode, the results might be different for each query, causing numbers to be inconsistent
with each other.
This context manager creates a transaction that is running in REPEATABLE READ mode to avoid this problem.
**You should only make read-only queries during this transaction and not rely on quota calculations.**
"""
is_under_test = 'tests.testdummy' in settings.INSTALLED_APPS
try:
with transaction.atomic(durable=not is_under_test):
if not is_under_test:
# We're not running this in tests, where we can basically not use this since the test runner does its
# own transaction logic for efficiency
with connection.cursor() as cursor:
if 'postgresql' in settings.DATABASES['default']['ENGINE']:
cursor.execute('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;')
elif 'sqlite' in settings.DATABASES['default']['ENGINE']:
pass # noop
else:
raise ImproperlyConfigured("Cannot set transaction isolation mode on this database backend")
connection.tx_in_repeatable_read = True
yield
finally:
connection.tx_in_repeatable_read = False
class GroupConcat(Aggregate):
function = 'group_concat'
template = '%(function)s(%(distinct)s%(field)s, "%(separator)s")'