Introduction
------------

I stumbled across a problem with nested transactions and created a
general fix before I found |this ticket|.  At its core, the solution I
developed is to change how ``manage`` interacts with
``enter_transaction_management``.  Currently, when
``enter_transaction_management`` is called, the previous state is
pushed onto the stack of states.  The procedure ``manage`` can change
this state at any time.  This solution forces the state of a
transactional context to remain managed if the outer context was
managed.

.. |this ticket| trac:: 2227

Basic Solution
--------------

* Make each item on the state stack a 2-tuple of ``(initialized,
  managed)``.  The ``initialized`` flag is used to make sure
  ``manage`` is only called once per block.  The ``managed`` flag is
  used by the ``is_managed`` procedure as before.

* Only allow ``manage`` to be called once per transaction block.  This
  prevents flip-flopping between a managed and unmanaged state and
  relieves ``manage`` of the responsibility of deciding whether to
  commit dirty transactions or not.

* Once a transaction block is set to ``managed = True``, nested
  transaction blocks are forced to be ``managed = True`` even if
  ``manage(False)`` is called.  This change prevents nested
  "autocommit" blocks from committing changes in an outer, manual
  transaction.

* ``connection._commit`` and ``connection._rollback`` are only called
  for top-level transactions or when there is not a current
  transaction.  ``commit`` and ``rollback`` operations are no-ops for
  nested transactions.  This is correct for ``commit``, but slightly
  unfortunate for ``rollback`` because a nested transaction block
  cannot roll itself back.  Such a partial rollback would require
  first-class support for save-points.

Other Changes
-------------

* ``connection._commit`` and ``connection._rollback`` are only called
  from ``commit`` and ``rollback`` respectively.

* The ``state`` and ``dirty`` dictionaries are replaced with a
  ``threading.local`` object (see Compatibility_).

* The patch from |ticket 7241| has been incorporated.

* |ticket 2304| could be fixed by adding a check for
   ``DISABLE_TRANSACTION_MANAGEMENT`` to
   ``TransactionState.really_transact``.

.. |ticket 2304| trac:: 2304
.. |ticket 7241| trac:: 7241

Compatibility
-------------

With this patch applied, Django's ``runtests.py`` passes all tests.
It is backward-compatible with un-nested transactions and database
operations lacking a transactional context (Django, by default,
autocommits).

It may not backward-compatible with:

1. outer transactions that roll back after an inner transaction has
   been committed
   
2. outer transactions that commit after an inner transaction has
   rolled back

A comments at the top of ``transaction.py`` implies that compatibility
with Python 2.3 is a goal.  If this is still the case, an
implementation of ``threading.local`` will need to be added to
``django.utils`` since ``threading.local`` was introduced in Python
2.4.  Such an implementation could use a technique similar to the
current technique (i.e. a dictionary indexed by thread-id).
Alternatively, the ``TransactionState`` class could be refactored to
not use ``threading.local`` as long as Python 2.3 is still supported.

Attachments
-----------

A patch is attached as well as a complete copy of the updated
``transaction.py``.  I've made some test cases.  The tests can be run
by unpacking the attached zip file and running ``./manage.py test``.
I've also attached ``test.py`` which has the relevant parts of the zip
file for easier browsing.
