Ticket #9964: 9964-make-managed-transactions-dirty-v3-full.patch

File 9964-make-managed-transactions-dirty-v3-full.patch, 19.0 KB (added by Shai Berger, 14 years ago)

Use deprecation warnings instead of errors for transactions left pending

  • django/db/backends/util.py

     
    5252    def __iter__(self):
    5353        return iter(self.cursor)
    5454
     55class CursorUseNotifyWrapper(object):
     56    def __init__(self, cursor, on_use):
     57        self.cursor = cursor
     58        self.on_use = on_use
     59    def __getattr__(self, attr):
     60        self.on_use()
     61        return getattr(self.cursor, attr)
     62    def __iter__(self):
     63        return iter(self.cursor)
     64
    5565###############################################
    5666# Converters from database (string) to Python #
    5767###############################################
  • django/db/backends/__init__.py

     
    2222        self.alias = alias
    2323        self.vendor = 'unknown'
    2424        self.use_debug_cursor = None
     25        self.on_used = None
    2526
    2627    def __eq__(self, other):
    2728        return self.alias == other.alias
     
    7576    def cursor(self):
    7677        from django.conf import settings
    7778        cursor = self._cursor()
     79        # Used by transaction mechanism to be notified that db was used
     80        if self.on_used:
     81            cursor = util.CursorUseNotifyWrapper(cursor, self.on_used)
    7882        if (self.use_debug_cursor or
    7983            (self.use_debug_cursor is None and settings.DEBUG)):
    8084            return self.make_debug_cursor(cursor)
  • django/db/transaction.py

     
    1212or implicit commits or rollbacks.
    1313"""
    1414import sys
     15from warnings import warn
    1516
    1617try:
    1718    import thread
     
    3233    """
    3334    pass
    3435
     36class ReadonlyTransactionWarning(PendingDeprecationWarning):
     37    """
     38    This is a warning issued when there is a difference between current behavior
     39    and future behavior due to read-only transactions being changed from clean
     40    to dirty.
     41    """
     42    pass
     43
    3544# The states are dictionaries of dictionaries of lists. The key to the outer
    3645# dict is the current thread, and the key to the inner dictionary is the
    3746# connection alias and the list is handled as a stack of values.
    3847state = {}
    3948savepoint_state = {}
    4049
    41 # The dirty flag is set by *_unless_managed functions to denote that the
    42 # code under transaction management has changed things to require a
     50# The dirty flag is set whenever a database cursor is touched to denote that the
     51# code under transaction management has done things to require a
    4352# database commit.
    4453# This is a dictionary mapping thread to a dictionary mapping connection
    4554# alias to a boolean.
    4655dirty = {}
     56# The deprecated_dirty flag is set by *_unless_managed functions. It is the old,
     57# slightly incorrect sense of dirty whereby read-only transactions may be left pending.
     58# This has the same structure as the dirty flag.
     59deprecated_dirty = {}
    4760
    4861def enter_transaction_management(managed=True, using=None):
    4962    """
     
    6780    if thread_ident not in dirty or using not in dirty[thread_ident]:
    6881        dirty.setdefault(thread_ident, {})
    6982        dirty[thread_ident][using] = False
     83    if thread_ident not in deprecated_dirty or using not in deprecated_dirty[thread_ident]:
     84        deprecated_dirty.setdefault(thread_ident, {})
     85        deprecated_dirty[thread_ident][using] = False
     86    def set_dirty_if_managed():
     87        if is_managed(using): _set_dirty(dirty, using)
     88    connection.on_used = set_dirty_if_managed
    7089    connection._enter_transaction_management(managed)
    7190
    7291def leave_transaction_management(using=None):
     
    84103        del state[thread_ident][using][-1]
    85104    else:
    86105        raise TransactionManagementError("This code isn't under transaction management")
    87     if dirty.get(thread_ident, {}).get(using, False):
     106    if deprecated_dirty.get(thread_ident, {}).get(using, False):
     107        # The transaction is dirty in the strict sense. This has always been an error
    88108        rollback(using=using)
    89109        raise TransactionManagementError("Transaction managed block ended with pending COMMIT/ROLLBACK")
     110    elif dirty.get(thread_ident, {}).get(using, False):
     111        #rollback(using=using)
     112        warn("Transaction managed block ended with pending COMMIT/ROLLBACK", ReadonlyTransactionWarning)
    90113    dirty[thread_ident][using] = False
    91114
     115def _is_dirty(dirty_dict, using=None):
     116    """
     117    Returns True if the current transaction requires a commit to close.
     118    """
     119    # TODO: Once deprecated_dirty is eliminated, fold this back into is_dirty
     120    if using is None:
     121        using = DEFAULT_DB_ALIAS
     122    return dirty_dict.get(thread.get_ident(), {}).get(using, False)
     123
    92124def is_dirty(using=None):
    93125    """
    94126    Returns True if the current transaction requires a commit for changes to
    95     happen.
     127    happen. Warns if no known changes happened, but a commit/rollback is
     128    still required.
    96129    """
    97130    if using is None:
    98131        using = DEFAULT_DB_ALIAS
    99     return dirty.get(thread.get_ident(), {}).get(using, False)
     132    flag = _is_dirty(deprecated_dirty, using)
     133    if flag != _is_dirty(dirty, using):
     134        warn("Read-only transactions will be considered dirty in the future",
     135             ReadonlyTransactionWarning)
     136    return flag
    100137
    101 def set_dirty(using=None):
     138def _set_dirty(dirty_dict, using=None):
    102139    """
    103140    Sets a dirty flag for the current thread and code streak. This can be used
    104141    to decide in a managed block of code to decide whether there are open
    105142    changes waiting for commit.
    106143    """
     144    # TODO: Once deprecated_dirty is eliminated, fold this back into set_dirty
    107145    if using is None:
    108146        using = DEFAULT_DB_ALIAS
    109147    thread_ident = thread.get_ident()
    110     if thread_ident in dirty and using in dirty[thread_ident]:
    111         dirty[thread_ident][using] = True
     148    if thread_ident in dirty_dict and using in dirty_dict[thread_ident]:
     149        dirty_dict[thread_ident][using] = True
    112150    else:
    113151        raise TransactionManagementError("This code isn't under transaction management")
    114152
    115 def set_clean(using=None):
     153def set_dirty(using=None):
    116154    """
     155    Sets a dirty flag for the current thread and code streak. This can be used
     156    to decide in a managed block of code to decide whether there are open
     157    changes waiting for commit.
     158    """
     159    _set_dirty(deprecated_dirty, using)
     160    _set_dirty(dirty, using)
     161
     162def _set_clean(dirty_dict, using=None):
     163    """
    117164    Resets a dirty flag for the current thread and code streak. This can be used
    118165    to decide in a managed block of code to decide whether a commit or rollback
    119166    should happen.
    120167    """
     168    # TODO: Once deprecated_dirty is eliminated, fold this back into set_clean
    121169    if using is None:
    122170        using = DEFAULT_DB_ALIAS
    123171    thread_ident = thread.get_ident()
    124     if thread_ident in dirty and using in dirty[thread_ident]:
    125         dirty[thread_ident][using] = False
     172    if thread_ident in dirty_dict and using in dirty_dict[thread_ident]:
     173        dirty_dict[thread_ident][using] = False
    126174    else:
    127175        raise TransactionManagementError("This code isn't under transaction management")
     176
     177def set_clean(using=None):
     178    _set_clean(deprecated_dirty, using)
     179    _set_clean(dirty, using)
    128180    clean_savepoints(using=using)
    129181
    130182def clean_savepoints(using=None):
     
    347399    def exiting(exc_value, using):
    348400        try:
    349401            if exc_value is not None:
    350                 if is_dirty(using=using):
     402                if _is_dirty(dirty, using=using):
    351403                    rollback(using=using)
    352404            else:
    353                 if is_dirty(using=using):
     405                if _is_dirty(dirty, using=using):
    354406                    try:
    355407                        commit(using=using)
    356408                    except:
  • django/middleware/transaction.py

     
    11from django.db import transaction
    22
     3def _transaction_is_dirty():
     4    """
     5    While the sense of a dirty transaction is being changed,
     6    this is the way to use the "new sense" (any touching of the
     7    db makes the transaction dirty)
     8    """
     9    # TODO: once transaction.deprecated_dirty is eliminated, delete this
     10    #       and use transaction.is_dirty instead
     11    return transaction._is_dirty(transaction.dirty)
     12
    313class TransactionMiddleware(object):
    414    """
    515    Transaction middleware. If this is enabled, each view function will be run
     
    1424
    1525    def process_exception(self, request, exception):
    1626        """Rolls back the database and leaves transaction management"""
    17         if transaction.is_dirty():
     27        if _transaction_is_dirty():
    1828            transaction.rollback()
    1929        transaction.leave_transaction_management()
    2030
    2131    def process_response(self, request, response):
    2232        """Commits and leaves transaction management."""
    2333        if transaction.is_managed():
    24             if transaction.is_dirty():
     34            if _transaction_is_dirty():
    2535                transaction.commit()
    2636            transaction.leave_transaction_management()
    2737        return response
     38
  • tests/regressiontests/transaction_regress/tests.py

     
     1from warnings import filterwarnings
     2from django.test import TransactionTestCase
     3from django.db import connection, transaction
     4from django.db.transaction import \
     5    commit_on_success, commit_manually, \
     6    TransactionManagementError, ReadonlyTransactionWarning
     7from models import Mod
     8
     9class Test9964(TransactionTestCase):
     10
     11    def setUp(self):
     12        filterwarnings("error", ".*", ReadonlyTransactionWarning)
     13       
     14    def test_raw_committed_on_success(self):
     15       
     16        @commit_on_success
     17        def raw_sql():
     18            cursor = connection.cursor()
     19            cursor.execute("INSERT into transaction_regress_mod (id,fld) values (17,18)")
     20           
     21        raw_sql()
     22        transaction.rollback()
     23        try:
     24            obj = Mod.objects.get(pk=17)
     25            self.assertEqual(obj.fld, 18)
     26        except Mod.DoesNotExist:
     27            self.fail("transaction with raw sql not committed")
     28
     29    def test_commit_manually_enforced(self):
     30        @commit_manually
     31        def non_comitter():
     32            Mod.objects.create(fld=55)
     33           
     34        self.assertRaises(TransactionManagementError, non_comitter)
     35
     36    def test_commit_manually_enforced_readonly(self):
     37        @commit_manually
     38        def non_comitter():
     39            _ = Mod.objects.count()
     40           
     41        self.assertRaises(ReadonlyTransactionWarning, non_comitter)
     42
     43    def test_commit_manually_commit_ok(self):
     44        @commit_manually
     45        def committer():
     46            _ = Mod.objects.count()
     47            transaction.commit()
     48       
     49        try:
     50            committer()
     51        except TransactionManagementError:
     52            self.fail("Commit did not clear the transaction state")
     53
     54    def test_commit_manually_rollback_ok(self):
     55        @commit_manually
     56        def roller_back():
     57            _ = Mod.objects.count()
     58            transaction.rollback()
     59       
     60        try:
     61            roller_back()
     62        except TransactionManagementError:
     63            self.fail("Rollback did not clear the transaction state")
     64
     65    def test_commit_manually_enforced_after_commit(self):
     66        @commit_manually
     67        def fake_committer():
     68            _ = Mod.objects.count()
     69            transaction.commit()
     70            _ = Mod.objects.count()           
     71           
     72        self.assertRaises(ReadonlyTransactionWarning, fake_committer)
     73
     74    def test_reuse_cursor_reference(self):
     75        @commit_on_success
     76        def reuse_cursor_ref():
     77            cursor = connection.cursor()
     78            cursor.execute("INSERT into transaction_regress_mod (id,fld) values (1,2)")
     79            transaction.rollback()
     80            cursor.execute("INSERT into transaction_regress_mod (id,fld) values (1,2)")
     81           
     82        reuse_cursor_ref()
     83        transaction.rollback()
     84        try:
     85            obj = Mod.objects.get(pk=1)
     86            self.assertEquals(obj.fld, 2)
     87        except Mod.DoesNotExist:
     88            self.fail("After ending a transaction, cursor use no longer sets dirty")
     89
     90    def test_6669(self):
     91
     92        from django.contrib.auth.models import User
     93
     94        @transaction.commit_on_success
     95        def create_system_user():
     96            user = User.objects.create_user(username='system', password='iamr00t', email='root@SITENAME.com')
     97            Mod.objects.create(fld=user.id)
     98
     99        try:
     100            create_system_user()
     101        except:
     102            pass
     103
     104        try:
     105            create_system_user()
     106        except:
     107            pass
     108
     109        print User.objects.all()
  • tests/regressiontests/transaction_regress/models.py

     
     1from django.db import models
     2
     3class Mod(models.Model):
     4    fld = models.IntegerField()
     5from django.db import models
     6
     7class Mod(models.Model):
     8    fld = models.IntegerField()
  • tests/regressiontests/delete_regress/tests.py

     
    6161        Book.objects.filter(pagecount__lt=250).delete()
    6262        transaction.commit()
    6363        self.assertEqual(1, Book.objects.count())
     64        transaction.commit()
    6465
    6566class DeleteCascadeTests(TestCase):
    6667    def test_generic_relation_cascade(self):
  • tests/regressiontests/fixtures_regress/tests.py

     
    610610        self.assertEqual(Thingy.objects.count(), 1)
    611611        transaction.rollback()
    612612        self.assertEqual(Thingy.objects.count(), 0)
     613        transaction.commit()
    613614
    614615    def test_ticket_11101(self):
    615616        """Test that fixtures can be rolled back (ticket #11101)."""
  • docs/topics/db/sql.txt

     
    231231
    232232Transactions and raw SQL
    233233------------------------
    234 If you are using transaction decorators (such as ``commit_on_success``) to
    235 wrap your views and provide transaction control, you don't have to make a
    236 manual call to ``transaction.commit_unless_managed()`` -- you can manually
    237 commit if you want to, but you aren't required to, since the decorator will
    238 commit for you. However, if you don't manually commit your changes, you will
    239 need to manually mark the transaction as dirty, using
    240 ``transaction.set_dirty()``::
    241234
    242     @commit_on_success
    243     def my_custom_sql_view(request, value):
    244         from django.db import connection, transaction
    245         cursor = connection.cursor()
     235.. versionchanged:: 1.3
     236   calling ``set_dirty()`` no longer required in managed transactions
    246237
    247         # Data modifying operation
    248         cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [value])
     238If you write functions that modify the database via raw SQL, you need to
     239consider the transaction management mode they run under. If this may be
     240Django's default mode (by default or using the ``auto_commit``
     241decorator), you need to make manual calls to
     242``transaction.commit_unless_managed()`` to make sure the changes are
     243committed. If you are using managed transactions (e.g. with the
     244``commit_on_success`` decorator), you don't have to make such calls.
    249245
    250         # Since we modified data, mark the transaction as dirty
    251         transaction.set_dirty()
    252246
    253         # Data retrieval operation. This doesn't dirty the transaction,
    254         # so no call to set_dirty() is required.
    255         cursor.execute("SELECT foo FROM bar WHERE baz = %s", [value])
    256         row = cursor.fetchone()
    257 
    258         return render_to_response('template.html', {'row': row})
    259 
    260 The call to ``set_dirty()`` is made automatically when you use the Django ORM
    261 to make data modifying database calls. However, when you use raw SQL, Django
    262 has no way of knowing if your SQL modifies data or not. The manual call to
    263 ``set_dirty()`` ensures that Django knows that there are modifications that
    264 must be committed.
    265 
    266247Connections and cursors
    267248-----------------------
    268249
  • docs/releases/1.3.txt

     
    301301    >>> formset = ArticleFormSet()
    302302    >>> formset = ArticleFormSet(data=None)
    303303
    304 
    305 
    306304.. _deprecated-features-1.3:
    307305
    308306Features deprecated in 1.3
     
    424422To ensure compatibility with future versions of Django, existing
    425423templates should be modified to use the new ``future`` libraries and
    426424syntax.
     425
     426Changes in transaction management
     427~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     428
     429When using managed transactions -- that is, anything but the default
     430autocommit mode -- it is important when a transaction is marked as
     431"dirty". Dirty transactions are committed by the ``commit_on_success``
     432decorator or the ``TransactionMiddleware``, and ``commit_manually``
     433forces them to be closed explicitly; clean transactions "get a pass",
     434which means they are usually rolled back at the end of a request
     435when the connection is closed.
     436
     437Until Django 1.3, transactions were only marked dirty when Django
     438was aware of a modifying operation performed in them; that is, either
     439some model was saved, some bulk update or delete was performed, or
     440the user explicitly called ``transaction.set_dirty()``. Django 1.3
     441starts a transistion to stricter semantics, where a transaction is
     442marked dirty when any database operation is performed;
     443this means you no longer need to set a transaction dirty explicitly
     444when you execute raw SQL or use a data-modifying ``select`` in a
     445managed transaction.
     446
     447On the other hand, the new semantics requires you to explicitly close
     448read-only transactions under ``commit_manually``, and implies a different
     449return value for ``django.db.transaction.is_dirty()`` in some
     450circumstances. These cases (read-only transactions left pending, and
     451calls to ``is_dirty()`` where the new semantics is different) issue a
     452``ReadonlyTransactionWarning`` in Django 1.3 and 1.4; in Django 1.5,
     453``is_dirty()`` will return the new value, and transctions left pending
     454will raise a ``TransactionManagementError``.
     455
Back to Top