summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Graham <timograham@gmail.com>2017-02-01 15:34:17 -0500
committerGitHub <noreply@github.com>2017-02-01 15:34:17 -0500
commit924af638e4d4fb8eb46a19ac0cafcb2e83480cf3 (patch)
tree61279859acd8f61260aa4f5fa1e3d569a2a73ae8
parentc4e18bb1cebc78e1e42c765608248ce3ead9deb7 (diff)
Fixed #27683 -- Made MySQL default to the read committed isolation level.
Thanks Shai Berger for test help and Adam Johnson for review.
-rw-r--r--django/db/backends/mysql/base.py2
-rw-r--r--docs/ref/databases.txt10
-rw-r--r--docs/releases/2.0.txt9
-rw-r--r--tests/backends/test_mysql.py9
-rw-r--r--tests/transactions/tests.py23
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
+<mysql-isolation-level>`, if needed.
+
:attr:`AbstractUser.last_name <django.contrib.auth.models.User.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()