diff --git a/django/db/transaction.py b/django/db/transaction.py index e78ed9b421..c769a4ba89 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -231,7 +231,13 @@ class Atomic(object): if sid is None: connection.needs_rollback = True else: - connection.savepoint_rollback(sid) + try: + connection.savepoint_rollback(sid) + except DatabaseError: + # If rolling back to a savepoint fails, mark for + # rollback at a higher level and avoid shadowing + # the original exception. + connection.needs_rollback = True else: # Roll back transaction connection.rollback() diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index 8b2e67a3cb..3ca6a22ecf 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -1,9 +1,15 @@ from __future__ import unicode_literals import sys +try: + import threading +except ImportError: + threading = None +import time from unittest import skipIf, skipUnless -from django.db import connection, transaction, DatabaseError, IntegrityError +from django.db import (connection, transaction, + DatabaseError, IntegrityError, OperationalError) from django.test import TransactionTestCase, skipIfDBFeature from django.utils import six @@ -333,6 +339,45 @@ class AtomicErrorsTests(TransactionTestCase): self.assertEqual(Reporter.objects.get(pk=r1.pk).last_name, "Calculus") +@skipUnless(connection.vendor == 'mysql', "MySQL-specific behaviors") +class AtomicMySQLTests(TransactionTestCase): + + available_apps = ['transactions'] + + @skipIf(threading is None, "Test requires threading") + def test_implicit_savepoint_rollback(self): + """MySQL implicitly rolls back savepoints when it deadlocks (#22291).""" + + other_thread_ready = threading.Event() + + def other_thread(): + try: + with transaction.atomic(): + Reporter.objects.create(id=1, first_name="Tintin") + other_thread_ready.set() + # We cannot synchronize the two threads with an event here + # because the main thread locks. Sleep for a little while. + time.sleep(1) + # 2) ... and this line deadlocks. (see below for 1) + Reporter.objects.exclude(id=1).update(id=2) + finally: + # This is the thread-local connection, not the main connection. + connection.close() + + other_thread = threading.Thread(target=other_thread) + other_thread.start() + other_thread_ready.wait() + + with six.assertRaisesRegex(self, OperationalError, 'Deadlock found'): + # Double atomic to enter a transaction and create a savepoint. + with transaction.atomic(): + with transaction.atomic(): + # 1) This line locks... (see above for 2) + Reporter.objects.create(id=1, first_name="Tintin") + + other_thread.join() + + class AtomicMiscTests(TransactionTestCase): available_apps = []