Fixed #27683 -- Made MySQL default to the read committed isolation level.
Thanks Shai Berger for test help and Adam Johnson for review.
This commit is contained in:
parent
c4e18bb1ce
commit
924af638e4
|
@ -217,7 +217,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
kwargs['client_flag'] = CLIENT.FOUND_ROWS
|
kwargs['client_flag'] = CLIENT.FOUND_ROWS
|
||||||
# Validate the transaction isolation level, if specified.
|
# Validate the transaction isolation level, if specified.
|
||||||
options = settings_dict['OPTIONS'].copy()
|
options = settings_dict['OPTIONS'].copy()
|
||||||
isolation_level = options.pop('isolation_level', None)
|
isolation_level = options.pop('isolation_level', 'read committed')
|
||||||
if isolation_level:
|
if isolation_level:
|
||||||
isolation_level = isolation_level.lower()
|
isolation_level = isolation_level.lower()
|
||||||
if isolation_level not in self.isolation_levels:
|
if isolation_level not in self.isolation_levels:
|
||||||
|
|
|
@ -449,8 +449,14 @@ this entry are the four standard isolation levels:
|
||||||
* ``'serializable'``
|
* ``'serializable'``
|
||||||
|
|
||||||
or ``None`` to use the server's configured isolation level. However, Django
|
or ``None`` to use the server's configured isolation level. However, Django
|
||||||
works best with read committed rather than MySQL's default, repeatable read.
|
works best with and defaults to read committed rather than MySQL's default,
|
||||||
Data loss is possible with repeatable read.
|
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
|
.. _transaction isolation level: https://dev.mysql.com/doc/refman/en/innodb-transaction-isolation-levels.html
|
||||||
|
|
||||||
|
|
|
@ -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
|
supported until April 2020 which almost reaches this date. Django 2.0
|
||||||
officially supports Oracle 12.1+.
|
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
|
:attr:`AbstractUser.last_name <django.contrib.auth.models.User.last_name>` ``max_length`` increased to 150
|
||||||
----------------------------------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -70,6 +70,15 @@ class MySQLTests(TestCase):
|
||||||
self.isolation_values[self.other_isolation_level]
|
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):
|
def test_isolation_level_validation(self):
|
||||||
new_connection = connection.copy()
|
new_connection = connection.copy()
|
||||||
new_connection.settings_dict['OPTIONS']['isolation_level'] = 'xxx'
|
new_connection.settings_dict['OPTIONS']['isolation_level'] = 'xxx'
|
||||||
|
|
|
@ -375,18 +375,17 @@ class AtomicMySQLTests(TransactionTestCase):
|
||||||
@skipIf(threading is None, "Test requires threading")
|
@skipIf(threading is None, "Test requires threading")
|
||||||
def test_implicit_savepoint_rollback(self):
|
def test_implicit_savepoint_rollback(self):
|
||||||
"""MySQL implicitly rolls back savepoints when it deadlocks (#22291)."""
|
"""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():
|
def other_thread():
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
Reporter.objects.create(id=1, first_name="Tintin")
|
Reporter.objects.select_for_update().get(id=1)
|
||||||
other_thread_ready.set()
|
main_thread_ready.wait()
|
||||||
# We cannot synchronize the two threads with an event here
|
# 1) This line locks... (see below for 2)
|
||||||
# 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)
|
Reporter.objects.exclude(id=1).update(id=2)
|
||||||
finally:
|
finally:
|
||||||
# This is the thread-local connection, not the main connection.
|
# 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 = threading.Thread(target=other_thread)
|
||||||
other_thread.start()
|
other_thread.start()
|
||||||
other_thread_ready.wait()
|
|
||||||
|
|
||||||
with self.assertRaisesMessage(OperationalError, 'Deadlock found'):
|
with self.assertRaisesMessage(OperationalError, 'Deadlock found'):
|
||||||
# Double atomic to enter a transaction and create a savepoint.
|
# Double atomic to enter a transaction and create a savepoint.
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# 1) This line locks... (see above for 2)
|
Reporter.objects.select_for_update().get(id=2)
|
||||||
Reporter.objects.create(id=1, first_name="Tintin")
|
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()
|
other_thread.join()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue