Skip to content

Commit

Permalink
MySQL Galera workaround (#416)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphaelm committed Feb 22, 2017
1 parent 0902014 commit 08e7a29
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 27 deletions.
4 changes: 4 additions & 0 deletions doc/admin/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
----

Expand Down
56 changes: 30 additions & 26 deletions src/pretix/base/views/async.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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')

Expand All @@ -31,10 +32,11 @@ def do(self, *args):
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):
Expand Down Expand Up @@ -64,24 +66,25 @@ def _return_ajax_result(self, res, timeout=.5):
'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):
Expand All @@ -90,10 +93,11 @@ def get_result(self, request):
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):
Expand Down
40 changes: 39 additions & 1 deletion src/pretix/helpers/database.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
1 change: 1 addition & 0 deletions src/pretix/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
'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/')

Expand Down

0 comments on commit 08e7a29

Please sign in to comment.