diff options
| author | Andreas Pelme <andreas@pelme.se> | 2015-06-30 18:18:56 +0200 |
|---|---|---|
| committer | Tim Graham <timograham@gmail.com> | 2015-06-30 14:51:00 -0400 |
| commit | 00a1d4d042a7afd139316982c9b57e87d26a894f (patch) | |
| tree | 65b4427112045acc25d097413b34faeab882ad88 /docs/topics/db/transactions.txt | |
| parent | 9f0d67137c98aa296471e1b7f57ae43f5bb17db6 (diff) | |
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.
Diffstat (limited to 'docs/topics/db/transactions.txt')
| -rw-r--r-- | docs/topics/db/transactions.txt | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 6b54510a47..b507b3ff67 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -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 ============== |
