From 924af638e4d4fb8eb46a19ac0cafcb2e83480cf3 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 1 Feb 2017 15:34:17 -0500 Subject: [PATCH] Fixed #27683 -- Made MySQL default to the read committed isolation level. Thanks Shai Berger for test help and Adam Johnson for review. --- django/db/backends/mysql/base.py | 2 +- docs/ref/databases.txt | 10 ++++++++-- docs/releases/2.0.txt | 9 +++++++++ tests/backends/test_mysql.py | 9 +++++++++ tests/transactions/tests.py | 23 +++++++++++++---------- 5 files changed, 40 insertions(+), 13 deletions(-) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 2f20b727ed..f9908dbcb5 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -217,7 +217,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): kwargs['client_flag'] = CLIENT.FOUND_ROWS # Validate the transaction isolation level, if specified. options = settings_dict['OPTIONS'].copy() - isolation_level = options.pop('isolation_level', None) + isolation_level = options.pop('isolation_level', 'read committed') if isolation_level: isolation_level = isolation_level.lower() if isolation_level not in self.isolation_levels: diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 26351314de..af368c8dd5 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -449,8 +449,14 @@ this entry are the four standard isolation levels: * ``'serializable'`` or ``None`` to use the server's configured isolation level. However, Django -works best with read committed rather than MySQL's default, repeatable read. -Data loss is possible with repeatable read. +works best with and defaults to read committed rather than MySQL's default, +repeatable read. Data loss is possible with repeatable read. + +.. versionchanged:: 2.0 + + In older versions, the MySQL database backend defaults to using the + database's isolation level (which defaults to repeatable read) rather + than read committed. .. _transaction isolation level: https://dev.mysql.com/doc/refman/en/innodb-transaction-isolation-levels.html diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index dec2b85228..4e42bd2b55 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -227,6 +227,15 @@ The end of upstream support for Oracle 11.2 is Dec. 2020. Django 1.11 will be supported until April 2020 which almost reaches this date. Django 2.0 officially supports Oracle 12.1+. +Default MySQL isolation level is read committed +----------------------------------------------- + +MySQL's default isolation level, repeatable read, may cause data loss in +typical Django usage. To prevent that and for consistency with other databases, +the default isolation level is now read committed. You can use the +:setting:`DATABASES` setting to :ref:`use a different isolation level +`, if needed. + :attr:`AbstractUser.last_name ` ``max_length`` increased to 150 ---------------------------------------------------------------------------------------------------------- diff --git a/tests/backends/test_mysql.py b/tests/backends/test_mysql.py index 637c3e377c..298ca9265f 100644 --- a/tests/backends/test_mysql.py +++ b/tests/backends/test_mysql.py @@ -70,6 +70,15 @@ class MySQLTests(TestCase): self.isolation_values[self.other_isolation_level] ) + def test_default_isolation_level(self): + # If not specified in settings, the default is read committed. + with get_connection() as new_connection: + new_connection.settings_dict['OPTIONS'].pop('isolation_level', None) + self.assertEqual( + self.get_isolation_level(new_connection), + self.isolation_values[self.read_committed] + ) + def test_isolation_level_validation(self): new_connection = connection.copy() new_connection.settings_dict['OPTIONS']['isolation_level'] = 'xxx' diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index 033619c0c8..8290bce1e5 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -375,18 +375,17 @@ class AtomicMySQLTests(TransactionTestCase): @skipIf(threading is None, "Test requires threading") def test_implicit_savepoint_rollback(self): """MySQL implicitly rolls back savepoints when it deadlocks (#22291).""" + Reporter.objects.create(id=1) + Reporter.objects.create(id=2) - other_thread_ready = threading.Event() + main_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.select_for_update().get(id=1) + main_thread_ready.wait() + # 1) This line locks... (see below for 2) Reporter.objects.exclude(id=1).update(id=2) finally: # This is the thread-local connection, not the main connection. @@ -394,14 +393,18 @@ class AtomicMySQLTests(TransactionTestCase): other_thread = threading.Thread(target=other_thread) other_thread.start() - other_thread_ready.wait() with self.assertRaisesMessage(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") + Reporter.objects.select_for_update().get(id=2) + main_thread_ready.set() + # The two threads can't be synchronized with an event here + # because the other thread locks. Sleep for a little while. + time.sleep(1) + # 2) ... and this line deadlocks. (see above for 1) + Reporter.objects.exclude(id=2).update(id=1) other_thread.join()