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:
Andreas Pelme 2015-06-30 18:18:56 +02:00 committed by Tim Graham
parent 9f0d67137c
commit 00a1d4d042
7 changed files with 462 additions and 15 deletions

View file

@ -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. Its a lot easier to undo something you never did in the
first place!
Low-level APIs
==============