diff --git a/doc/admin/config.rst b/doc/admin/config.rst index 16c7b72a7..7da1046ce 100644 --- a/doc/admin/config.rst +++ b/doc/admin/config.rst @@ -102,6 +102,10 @@ Example:: ``user``, ``password``, ``host``, ``port`` Connection details for the database connection. Empty by default. +``galera`` + Indicates if the database backend is a MySQL/MariaDB Galera cluster and + turns on some optimizations/special case handlers. Default: ``False`` + URLs ---- diff --git a/src/pretix/base/views/async.py b/src/pretix/base/views/async.py index 4b76db552..1c0b1760f 100644 --- a/src/pretix/base/views/async.py +++ b/src/pretix/base/views/async.py @@ -9,6 +9,7 @@ from django.shortcuts import redirect, render from django.utils.translation import ugettext as _ from pretix.celery_app import app +from pretix.helpers.database import casual_reads logger = logging.getLogger('pretix.base.async') @@ -31,10 +32,11 @@ class AsyncAction: return JsonResponse(data) else: if res.ready(): - if res.successful() and not isinstance(res.info, Exception): - return self.success(res.info) - else: - return self.error(res.info) + with casual_reads(): + if res.successful() and not isinstance(res.info, Exception): + 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): @@ -64,24 +66,25 @@ class AsyncAction: 'ready': ready } if ready: - if res.successful() and not isinstance(res.info, Exception): - smes = self.get_success_message(res.info) - if smes: - messages.success(self.request, smes) - # 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), - 'message': str(self.get_success_message(res.info)) - }) - else: - messages.error(self.request, self.get_error_message(res.info)) - # 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(), - 'message': str(self.get_error_message(res.info)) - }) + with casual_reads(): + if res.successful() and not isinstance(res.info, Exception): + smes = self.get_success_message(res.info) + if smes: + messages.success(self.request, smes) + # 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), + 'message': str(self.get_success_message(res.info)) + }) + else: + messages.error(self.request, self.get_error_message(res.info)) + # 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(), + 'message': str(self.get_error_message(res.info)) + }) return data def get_result(self, request): @@ -90,10 +93,11 @@ class AsyncAction: return JsonResponse(self._return_ajax_result(res, timeout=0.25)) else: if res.ready(): - if res.successful() and not isinstance(res.info, Exception): - return self.success(res.info) - else: - return self.error(res.info) + with casual_reads(): + if res.successful() and not isinstance(res.info, Exception): + return self.success(res.info) + else: + return self.error(res.info) return render(request, 'pretixpresale/waiting.html') def success(self, value): diff --git a/src/pretix/helpers/database.py b/src/pretix/helpers/database.py index ddd695a82..910ea8807 100644 --- a/src/pretix/helpers/database.py +++ b/src/pretix/helpers/database.py @@ -1,6 +1,7 @@ import contextlib -from django.db import transaction +from django.conf import settings +from django.db import connection, transaction class DummyRollbackException(Exception): @@ -26,3 +27,40 @@ def rolledback_transaction(): pass else: raise Exception('Invalid state, should have rolled back.') + + +if 'mysql' in settings.DATABASES['default']['ENGINE'] and settings.DATABASE_IS_GALERA: + + @contextlib.contextmanager + def casual_reads(): + """ + When pretix runs with a MySQL galera cluster as a database backend, we can run into the + following problem: + + * A celery thread starts a transaction, creates an object and commits the transaction. + It then returns the object ID into celery's result store (e.g. redis) + + * A web thread pulls the object ID from the result store, but cannot access the object + yet as the transaction is not yet committed everywhere. + + This sets the wsrep_sync_wait variable to deal with this problem. + + See also: + + * https://mariadb.com/kb/en/mariadb/galera-cluster-system-variables/#wsrep_sync_wait + + * https://www.percona.com/doc/percona-xtradb-cluster/5.6/wsrep-system-index.html#wsrep_sync_wait + """ + with connection.cursor() as cursor: + cursor.execute("SET @wsrep_sync_wait_orig = @@wsrep_sync_wait;") + cursor.execute("SET SESSION wsrep_sync_wait = GREATEST(@wsrep_sync_wait_orig, 1);") + try: + yield + finally: + cursor.execute("SET SESSION wsrep_sync_wait = @wsrep_sync_wait_orig;") + +else: + + @contextlib.contextmanager + def casual_reads(): + yield diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 2f427454c..e1854ad5e 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -58,6 +58,7 @@ DATABASES = { 'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120 } } +DATABASE_IS_GALERA = config.getboolean('database', 'galera', fallback=False) STATIC_URL = config.get('urls', 'static', fallback='/static/')