mirror of https://github.com/django/django.git
Fixed #32220 -- Added durable argument to transaction.atomic().
This commit is contained in:
parent
8b040e3cbb
commit
3828879eee
|
@ -158,16 +158,30 @@ class Atomic(ContextDecorator):
|
||||||
|
|
||||||
Since database connections are thread-local, this is thread-safe.
|
Since database connections are thread-local, this is thread-safe.
|
||||||
|
|
||||||
|
An atomic block can be tagged as durable. In this case, raise a
|
||||||
|
RuntimeError if it's nested within another atomic block. This guarantees
|
||||||
|
that database changes in a durable block are committed to the database when
|
||||||
|
the block exists without error.
|
||||||
|
|
||||||
This is a private API.
|
This is a private API.
|
||||||
"""
|
"""
|
||||||
|
# This private flag is provided only to disable the durability checks in
|
||||||
|
# TestCase.
|
||||||
|
_ensure_durability = True
|
||||||
|
|
||||||
def __init__(self, using, savepoint):
|
def __init__(self, using, savepoint, durable):
|
||||||
self.using = using
|
self.using = using
|
||||||
self.savepoint = savepoint
|
self.savepoint = savepoint
|
||||||
|
self.durable = durable
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
connection = get_connection(self.using)
|
connection = get_connection(self.using)
|
||||||
|
|
||||||
|
if self.durable and self._ensure_durability and connection.in_atomic_block:
|
||||||
|
raise RuntimeError(
|
||||||
|
'A durable atomic block cannot be nested within another '
|
||||||
|
'atomic block.'
|
||||||
|
)
|
||||||
if not connection.in_atomic_block:
|
if not connection.in_atomic_block:
|
||||||
# Reset state when entering an outermost atomic block.
|
# Reset state when entering an outermost atomic block.
|
||||||
connection.commit_on_exit = True
|
connection.commit_on_exit = True
|
||||||
|
@ -282,14 +296,14 @@ class Atomic(ContextDecorator):
|
||||||
connection.in_atomic_block = False
|
connection.in_atomic_block = False
|
||||||
|
|
||||||
|
|
||||||
def atomic(using=None, savepoint=True):
|
def atomic(using=None, savepoint=True, durable=False):
|
||||||
# Bare decorator: @atomic -- although the first argument is called
|
# Bare decorator: @atomic -- although the first argument is called
|
||||||
# `using`, it's actually the function being decorated.
|
# `using`, it's actually the function being decorated.
|
||||||
if callable(using):
|
if callable(using):
|
||||||
return Atomic(DEFAULT_DB_ALIAS, savepoint)(using)
|
return Atomic(DEFAULT_DB_ALIAS, savepoint, durable)(using)
|
||||||
# Decorator: @atomic(...) or context manager: with atomic(...): ...
|
# Decorator: @atomic(...) or context manager: with atomic(...): ...
|
||||||
else:
|
else:
|
||||||
return Atomic(using, savepoint)
|
return Atomic(using, savepoint, durable)
|
||||||
|
|
||||||
|
|
||||||
def _non_atomic_requests(view, using):
|
def _non_atomic_requests(view, using):
|
||||||
|
|
|
@ -1181,29 +1181,37 @@ class TestCase(TransactionTestCase):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
if not cls._databases_support_transactions():
|
if not cls._databases_support_transactions():
|
||||||
return
|
return
|
||||||
cls.cls_atomics = cls._enter_atomics()
|
# Disable the durability check to allow testing durable atomic blocks
|
||||||
|
# in a transaction for performance reasons.
|
||||||
if cls.fixtures:
|
transaction.Atomic._ensure_durability = False
|
||||||
for db_name in cls._databases_names(include_mirrors=False):
|
|
||||||
try:
|
|
||||||
call_command('loaddata', *cls.fixtures, **{'verbosity': 0, 'database': db_name})
|
|
||||||
except Exception:
|
|
||||||
cls._rollback_atomics(cls.cls_atomics)
|
|
||||||
cls._remove_databases_failures()
|
|
||||||
raise
|
|
||||||
pre_attrs = cls.__dict__.copy()
|
|
||||||
try:
|
try:
|
||||||
cls.setUpTestData()
|
cls.cls_atomics = cls._enter_atomics()
|
||||||
|
|
||||||
|
if cls.fixtures:
|
||||||
|
for db_name in cls._databases_names(include_mirrors=False):
|
||||||
|
try:
|
||||||
|
call_command('loaddata', *cls.fixtures, **{'verbosity': 0, 'database': db_name})
|
||||||
|
except Exception:
|
||||||
|
cls._rollback_atomics(cls.cls_atomics)
|
||||||
|
cls._remove_databases_failures()
|
||||||
|
raise
|
||||||
|
pre_attrs = cls.__dict__.copy()
|
||||||
|
try:
|
||||||
|
cls.setUpTestData()
|
||||||
|
except Exception:
|
||||||
|
cls._rollback_atomics(cls.cls_atomics)
|
||||||
|
cls._remove_databases_failures()
|
||||||
|
raise
|
||||||
|
for name, value in cls.__dict__.items():
|
||||||
|
if value is not pre_attrs.get(name):
|
||||||
|
setattr(cls, name, TestData(name, value))
|
||||||
except Exception:
|
except Exception:
|
||||||
cls._rollback_atomics(cls.cls_atomics)
|
transaction.Atomic._ensure_durability = True
|
||||||
cls._remove_databases_failures()
|
|
||||||
raise
|
raise
|
||||||
for name, value in cls.__dict__.items():
|
|
||||||
if value is not pre_attrs.get(name):
|
|
||||||
setattr(cls, name, TestData(name, value))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
|
transaction.Atomic._ensure_durability = True
|
||||||
if cls._databases_support_transactions():
|
if cls._databases_support_transactions():
|
||||||
cls._rollback_atomics(cls.cls_atomics)
|
cls._rollback_atomics(cls.cls_atomics)
|
||||||
for conn in connections.all():
|
for conn in connections.all():
|
||||||
|
|
|
@ -356,6 +356,11 @@ Models
|
||||||
allow using transforms. See :ref:`using-transforms-in-expressions` for
|
allow using transforms. See :ref:`using-transforms-in-expressions` for
|
||||||
details.
|
details.
|
||||||
|
|
||||||
|
* The new ``durable`` argument for :func:`~django.db.transaction.atomic`
|
||||||
|
guarantees that changes made in the atomic block will be committed if the
|
||||||
|
block exits without errors. A nested atomic block marked as durable will
|
||||||
|
raise a ``RuntimeError``.
|
||||||
|
|
||||||
Pagination
|
Pagination
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ Controlling transactions explicitly
|
||||||
|
|
||||||
Django provides a single API to control database transactions.
|
Django provides a single API to control database transactions.
|
||||||
|
|
||||||
.. function:: atomic(using=None, savepoint=True)
|
.. function:: atomic(using=None, savepoint=True, durable=False)
|
||||||
|
|
||||||
Atomicity is the defining property of database transactions. ``atomic``
|
Atomicity is the defining property of database transactions. ``atomic``
|
||||||
allows us to create a block of code within which the atomicity on the
|
allows us to create a block of code within which the atomicity on the
|
||||||
|
@ -105,6 +105,12 @@ Django provides a single API to control database transactions.
|
||||||
completes successfully, its effects can still be rolled back if an
|
completes successfully, its effects can still be rolled back if an
|
||||||
exception is raised in the outer block at a later point.
|
exception is raised in the outer block at a later point.
|
||||||
|
|
||||||
|
It is sometimes useful to ensure an ``atomic`` block is always the
|
||||||
|
outermost ``atomic`` block, ensuring that any database changes are
|
||||||
|
committed when the block is exited without errors. This is known as
|
||||||
|
durability and can be achieved by setting ``durable=True``. If the
|
||||||
|
``atomic`` block is nested within another it raises a ``RuntimeError``.
|
||||||
|
|
||||||
``atomic`` is usable both as a :py:term:`decorator`::
|
``atomic`` is usable both as a :py:term:`decorator`::
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
@ -232,6 +238,16 @@ Django provides a single API to control database transactions.
|
||||||
is especially important if you're using :func:`atomic` in long-running
|
is especially important if you're using :func:`atomic` in long-running
|
||||||
processes, outside of Django's request / response cycle.
|
processes, outside of Django's request / response cycle.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
:class:`django.test.TestCase` disables the durability check to allow
|
||||||
|
testing durable atomic blocks in a transaction for performance reasons. Use
|
||||||
|
:class:`django.test.TransactionTestCase` for testing durability.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
The ``durable`` argument was added.
|
||||||
|
|
||||||
Autocommit
|
Autocommit
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.db import (
|
||||||
transaction,
|
transaction,
|
||||||
)
|
)
|
||||||
from django.test import (
|
from django.test import (
|
||||||
TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
|
TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .models import Reporter
|
from .models import Reporter
|
||||||
|
@ -498,3 +498,76 @@ class NonAutocommitTests(TransactionTestCase):
|
||||||
finally:
|
finally:
|
||||||
transaction.rollback()
|
transaction.rollback()
|
||||||
transaction.set_autocommit(True)
|
transaction.set_autocommit(True)
|
||||||
|
|
||||||
|
|
||||||
|
class DurableTests(TransactionTestCase):
|
||||||
|
available_apps = ['transactions']
|
||||||
|
|
||||||
|
def test_commit(self):
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
reporter = Reporter.objects.create(first_name='Tintin')
|
||||||
|
self.assertEqual(Reporter.objects.get(), reporter)
|
||||||
|
|
||||||
|
def test_nested_outer_durable(self):
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
reporter1 = Reporter.objects.create(first_name='Tintin')
|
||||||
|
with transaction.atomic():
|
||||||
|
reporter2 = Reporter.objects.create(
|
||||||
|
first_name='Archibald',
|
||||||
|
last_name='Haddock',
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(Reporter.objects.all(), [reporter2, reporter1])
|
||||||
|
|
||||||
|
def test_nested_both_durable(self):
|
||||||
|
msg = 'A durable atomic block cannot be nested within another atomic block.'
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
with self.assertRaisesMessage(RuntimeError, msg):
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_nested_inner_durable(self):
|
||||||
|
msg = 'A durable atomic block cannot be nested within another atomic block.'
|
||||||
|
with transaction.atomic():
|
||||||
|
with self.assertRaisesMessage(RuntimeError, msg):
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DisableDurabiltityCheckTests(TestCase):
|
||||||
|
"""
|
||||||
|
TestCase runs all tests in a transaction by default. Code using
|
||||||
|
durable=True would always fail when run from TestCase. This would mean
|
||||||
|
these tests would be forced to use the slower TransactionTestCase even when
|
||||||
|
not testing durability. For this reason, TestCase disables the durability
|
||||||
|
check.
|
||||||
|
"""
|
||||||
|
available_apps = ['transactions']
|
||||||
|
|
||||||
|
def test_commit(self):
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
reporter = Reporter.objects.create(first_name='Tintin')
|
||||||
|
self.assertEqual(Reporter.objects.get(), reporter)
|
||||||
|
|
||||||
|
def test_nested_outer_durable(self):
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
reporter1 = Reporter.objects.create(first_name='Tintin')
|
||||||
|
with transaction.atomic():
|
||||||
|
reporter2 = Reporter.objects.create(
|
||||||
|
first_name='Archibald',
|
||||||
|
last_name='Haddock',
|
||||||
|
)
|
||||||
|
self.assertSequenceEqual(Reporter.objects.all(), [reporter2, reporter1])
|
||||||
|
|
||||||
|
def test_nested_both_durable(self):
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
# Error is not raised.
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
reporter = Reporter.objects.create(first_name='Tintin')
|
||||||
|
self.assertEqual(Reporter.objects.get(), reporter)
|
||||||
|
|
||||||
|
def test_nested_inner_durable(self):
|
||||||
|
with transaction.atomic():
|
||||||
|
# Error is not raised.
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
reporter = Reporter.objects.create(first_name='Tintin')
|
||||||
|
self.assertEqual(Reporter.objects.get(), reporter)
|
||||||
|
|
Loading…
Reference in New Issue