From ab61a9b190d09b638e3711d47fa09de8fedd9617 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 31 Mar 2021 11:32:54 +0200 Subject: [PATCH] Guard against integrity errors when saving questions --- src/pretix/base/views/mixins.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/pretix/base/views/mixins.py b/src/pretix/base/views/mixins.py index edc12fb297..e67f73ec9c 100644 --- a/src/pretix/base/views/mixins.py +++ b/src/pretix/base/views/mixins.py @@ -4,6 +4,7 @@ from decimal import Decimal from django import forms from django.core.files.uploadedfile import UploadedFile +from django.db import IntegrityError from django.db.models import Prefetch, QuerySet from django.utils.functional import cached_property @@ -148,8 +149,24 @@ class BaseQuestionsViewMixin: orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None), question=field.question, ) - self._save_to_answer(field, answer, v) - answer.save() + try: + self._save_to_answer(field, answer, v) + answer.save() + except IntegrityError: + # Since we prefill ``field.answer`` at form creation time, there's a possible race condition + # here if the users submits their save request a second time while the first one is still running, + # thus leading to duplicate QuestionAnswer objects. Since Django doesn't support UPSERT, the "proper" + # fix would be a transaction with select_for_update(), or at least fetching using get_or_create here + # again. However, both of these approaches have a significant performance overhead for *all* requests, + # while the issue happens very very rarely. So we opt for just catching the error and retrying properly. + answer = QuestionAnswer.objects.get( + cartposition=(form.pos if isinstance(form.pos, CartPosition) else None), + orderposition=(form.pos if isinstance(form.pos, OrderPosition) else None), + question=field.question, + ) + self._save_to_answer(field, answer, v) + answer.save() + else: meta_info.setdefault('question_form_data', {}) if v is None: