mirror of
https://github.com/django/django.git
synced 2025-08-03 10:34:04 +00:00
Fixed #21803 -- Added support for post-commit callbacks
Made it possible to register and run callbacks after a database transaction is committed with the `transaction.on_commit()` function. This patch is heavily based on Carl Meyers django-transaction-hooks <https://django-transaction-hooks.readthedocs.org/>. Thanks to Aymeric Augustin, Carl Meyer, and Tim Graham for review and feedback.
This commit is contained in:
parent
9f0d67137c
commit
00a1d4d042
7 changed files with 462 additions and 15 deletions
|
@ -252,6 +252,150 @@ by Django or by third-party libraries. Thus, this is best used in situations
|
|||
where you want to run your own transaction-controlling middleware or do
|
||||
something really strange.
|
||||
|
||||
Performing actions after commit
|
||||
===============================
|
||||
|
||||
.. versionadded:: 1.9
|
||||
|
||||
Sometimes you need to perform an action related to the current database
|
||||
transaction, but only if the transaction successfully commits. Examples might
|
||||
include a `Celery`_ task, an email notification, or a cache invalidation.
|
||||
|
||||
.. _Celery: http://www.celeryproject.org/
|
||||
|
||||
Django provides the :func:`on_commit` function to register callback functions
|
||||
that should be executed after a transaction is successfully committed:
|
||||
|
||||
.. function:: on_commit(func, using=None)
|
||||
|
||||
Pass any function (that takes no arguments) to :func:`on_commit`::
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
def do_something():
|
||||
pass # send a mail, invalidate a cache, fire off a Celery task, etc.
|
||||
|
||||
transaction.on_commit(do_something)
|
||||
|
||||
You can also wrap your function in a lambda::
|
||||
|
||||
transaction.on_commit(lambda: some_celery_task.delay('arg1'))
|
||||
|
||||
The function you pass in will be called immediately after a hypothetical
|
||||
database write made where ``on_commit()`` is called would be successfully
|
||||
committed.
|
||||
|
||||
If you call ``on_commit()`` while there isn't an active transaction, the
|
||||
callback will be executed immediately.
|
||||
|
||||
If that hypothetical database write is instead rolled back (typically when an
|
||||
unhandled exception is raised in an :func:`atomic` block), your function will
|
||||
be discarded and never called.
|
||||
|
||||
Savepoints
|
||||
----------
|
||||
|
||||
Savepoints (i.e. nested :func:`atomic` blocks) are handled correctly. That is,
|
||||
an :func:`on_commit` callable registered after a savepoint (in a nested
|
||||
:func:`atomic` block) will be called after the outer transaction is committed,
|
||||
but not if a rollback to that savepoint or any previous savepoint occurred
|
||||
during the transaction::
|
||||
|
||||
with transaction.atomic(): # Outer atomic, start a new transaction
|
||||
transaction.on_commit(foo)
|
||||
|
||||
with transaction.atomic(): # Inner atomic block, create a savepoint
|
||||
transaction.on_commit(bar)
|
||||
|
||||
# foo() and then bar() will be called when leaving the outermost block
|
||||
|
||||
On the other hand, when a savepoint is rolled back (due to an exception being
|
||||
raised), the inner callable will not be called::
|
||||
|
||||
with transaction.atomic(): # Outer atomic, start a new transaction
|
||||
transaction.on_commit(foo)
|
||||
|
||||
try:
|
||||
with transaction.atomic(): # Inner atomic block, create a savepoint
|
||||
transaction.on_commit(bar)
|
||||
raise SomeError() # Raising an exception - abort the savepoint
|
||||
except SomeError:
|
||||
pass
|
||||
|
||||
# foo() will be called, but not bar()
|
||||
|
||||
Order of execution
|
||||
------------------
|
||||
|
||||
On-commit functions for a given transaction are executed in the order they were
|
||||
registered.
|
||||
|
||||
Exception handling
|
||||
------------------
|
||||
|
||||
If one on-commit function within a given transaction raises an uncaught
|
||||
exception, no later registered functions in that same transaction will run.
|
||||
This is, of course, the same behavior as if you'd executed the functions
|
||||
sequentially yourself without :func:`on_commit`.
|
||||
|
||||
Timing of execution
|
||||
-------------------
|
||||
|
||||
Your callbacks are executed *after* a successful commit, so a failure in a
|
||||
callback will not cause the transaction to roll back. They are executed
|
||||
conditionally upon the success of the transaction, but they are not *part* of
|
||||
the transaction. For the intended use cases (mail notifications, Celery tasks,
|
||||
etc.), this should be fine. If it's not (if your follow-up action is so
|
||||
critical that its failure should mean the failure of the transaction itself),
|
||||
then you don't want to use the :func:`on_commit` hook. Instead, you may want
|
||||
`two-phase commit`_ such as the `psycopg Two-Phase Commit protocol support`_
|
||||
and the `optional Two-Phase Commit Extensions in the Python DB-API
|
||||
specification`_.
|
||||
|
||||
Callbacks are not run until autocommit is restored on the connection following
|
||||
the commit (because otherwise any queries done in a callback would open an
|
||||
implicit transaction, preventing the connection from going back into autocommit
|
||||
mode).
|
||||
|
||||
When in autocommit mode and outside of an :func:`atomic` block, the function
|
||||
will run immediately, not on commit.
|
||||
|
||||
On-commit functions only work with :ref:`autocommit mode <managing-autocommit>`
|
||||
and the :func:`atomic` (or :setting:`ATOMIC_REQUESTS
|
||||
<DATABASE-ATOMIC_REQUESTS>`) transaction API. Calling :func:`on_commit` when
|
||||
autocommit is disabled and you are not within an atomic block will result in an
|
||||
error.
|
||||
|
||||
.. _two-phase commit: http://en.wikipedia.org/wiki/Two-phase_commit_protocol
|
||||
.. _psycopg Two-Phase Commit protocol support: http://initd.org/psycopg/docs/usage.html#tpc
|
||||
.. _optional Two-Phase Commit Extensions in the Python DB-API specification: https://www.python.org/dev/peps/pep-0249/#optional-two-phase-commit-extensions
|
||||
|
||||
Use in tests
|
||||
------------
|
||||
|
||||
Django's :class:`~django.test.TestCase` class wraps each test in a transaction
|
||||
and rolls back that transaction after each test, in order to provide test
|
||||
isolation. This means that no transaction is ever actually committed, thus your
|
||||
:func:`on_commit` callbacks will never be run. If you need to test the results
|
||||
of an :func:`on_commit` callback, use a
|
||||
:class:`~django.test.TransactionTestCase` instead.
|
||||
|
||||
Why no rollback hook?
|
||||
---------------------
|
||||
|
||||
A rollback hook is harder to implement robustly than a commit hook, since a
|
||||
variety of things can cause an implicit rollback.
|
||||
|
||||
For instance, if your database connection is dropped because your process was
|
||||
killed without a chance to shut down gracefully, your rollback hook will never
|
||||
run.
|
||||
|
||||
The solution is simple: instead of doing something during the atomic block
|
||||
(transaction) and then undoing it if the transaction fails, use
|
||||
:func:`on_commit` to delay doing it in the first place until after the
|
||||
transaction succeeds. It’s a lot easier to undo something you never did in the
|
||||
first place!
|
||||
|
||||
Low-level APIs
|
||||
==============
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue