web/lib/django/db/transaction.py
changeset 38 77b6da96e6f1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/lib/django/db/transaction.py	Wed Jun 02 18:57:35 2010 +0200
@@ -0,0 +1,351 @@
+"""
+This module implements a transaction manager that can be used to define
+transaction handling in a request or view function. It is used by transaction
+control middleware and decorators.
+
+The transaction manager can be in managed or in auto state. Auto state means the
+system is using a commit-on-save strategy (actually it's more like
+commit-on-change). As soon as the .save() or .delete() (or related) methods are
+called, a commit is made.
+
+Managed transactions don't do those commits, but will need some kind of manual
+or implicit commits or rollbacks.
+"""
+
+try:
+    import thread
+except ImportError:
+    import dummy_thread as thread
+try:
+    from functools import wraps
+except ImportError:
+    from django.utils.functional import wraps  # Python 2.4 fallback.
+from django.db import connections, DEFAULT_DB_ALIAS
+from django.conf import settings
+
+class TransactionManagementError(Exception):
+    """
+    This exception is thrown when something bad happens with transaction
+    management.
+    """
+    pass
+
+# The states are dictionaries of dictionaries of lists. The key to the outer
+# dict is the current thread, and the key to the inner dictionary is the
+# connection alias and the list is handled as a stack of values.
+state = {}
+savepoint_state = {}
+
+# The dirty flag is set by *_unless_managed functions to denote that the
+# code under transaction management has changed things to require a
+# database commit.
+# This is a dictionary mapping thread to a dictionary mapping connection
+# alias to a boolean.
+dirty = {}
+
+def enter_transaction_management(managed=True, using=None):
+    """
+    Enters transaction management for a running thread. It must be balanced with
+    the appropriate leave_transaction_management call, since the actual state is
+    managed as a stack.
+
+    The state and dirty flag are carried over from the surrounding block or
+    from the settings, if there is no surrounding block (dirty is always false
+    when no current block is running).
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    thread_ident = thread.get_ident()
+    if thread_ident in state and state[thread_ident].get(using):
+        state[thread_ident][using].append(state[thread_ident][using][-1])
+    else:
+        state.setdefault(thread_ident, {})
+        state[thread_ident][using] = [settings.TRANSACTIONS_MANAGED]
+    if thread_ident not in dirty or using not in dirty[thread_ident]:
+        dirty.setdefault(thread_ident, {})
+        dirty[thread_ident][using] = False
+    connection._enter_transaction_management(managed)
+
+def leave_transaction_management(using=None):
+    """
+    Leaves transaction management for a running thread. A dirty flag is carried
+    over to the surrounding block, as a commit will commit all changes, even
+    those from outside. (Commits are on connection level.)
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    connection._leave_transaction_management(is_managed(using=using))
+    thread_ident = thread.get_ident()
+    if thread_ident in state and state[thread_ident].get(using):
+        del state[thread_ident][using][-1]
+    else:
+        raise TransactionManagementError("This code isn't under transaction management")
+    if dirty.get(thread_ident, {}).get(using, False):
+        rollback(using=using)
+        raise TransactionManagementError("Transaction managed block ended with pending COMMIT/ROLLBACK")
+    dirty[thread_ident][using] = False
+
+def is_dirty(using=None):
+    """
+    Returns True if the current transaction requires a commit for changes to
+    happen.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    return dirty.get(thread.get_ident(), {}).get(using, False)
+
+def set_dirty(using=None):
+    """
+    Sets a dirty flag for the current thread and code streak. This can be used
+    to decide in a managed block of code to decide whether there are open
+    changes waiting for commit.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    thread_ident = thread.get_ident()
+    if thread_ident in dirty and using in dirty[thread_ident]:
+        dirty[thread_ident][using] = True
+    else:
+        raise TransactionManagementError("This code isn't under transaction management")
+
+def set_clean(using=None):
+    """
+    Resets a dirty flag for the current thread and code streak. This can be used
+    to decide in a managed block of code to decide whether a commit or rollback
+    should happen.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    thread_ident = thread.get_ident()
+    if thread_ident in dirty and using in dirty[thread_ident]:
+        dirty[thread_ident][using] = False
+    else:
+        raise TransactionManagementError("This code isn't under transaction management")
+    clean_savepoints(using=using)
+
+def clean_savepoints(using=None):
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    thread_ident = thread.get_ident()
+    if thread_ident in savepoint_state and using in savepoint_state[thread_ident]:
+        del savepoint_state[thread_ident][using]
+
+def is_managed(using=None):
+    """
+    Checks whether the transaction manager is in manual or in auto state.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    thread_ident = thread.get_ident()
+    if thread_ident in state and using in state[thread_ident]:
+        if state[thread_ident][using]:
+            return state[thread_ident][using][-1]
+    return settings.TRANSACTIONS_MANAGED
+
+def managed(flag=True, using=None):
+    """
+    Puts the transaction manager into a manual state: managed transactions have
+    to be committed explicitly by the user. If you switch off transaction
+    management and there is a pending commit/rollback, the data will be
+    commited.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    thread_ident = thread.get_ident()
+    top = state.get(thread_ident, {}).get(using, None)
+    if top:
+        top[-1] = flag
+        if not flag and is_dirty(using=using):
+            connection._commit()
+            set_clean(using=using)
+    else:
+        raise TransactionManagementError("This code isn't under transaction management")
+
+def commit_unless_managed(using=None):
+    """
+    Commits changes if the system is not in managed transaction mode.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    if not is_managed(using=using):
+        connection._commit()
+        clean_savepoints(using=using)
+    else:
+        set_dirty(using=using)
+
+def rollback_unless_managed(using=None):
+    """
+    Rolls back changes if the system is not in managed transaction mode.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    if not is_managed(using=using):
+        connection._rollback()
+    else:
+        set_dirty(using=using)
+
+def commit(using=None):
+    """
+    Does the commit itself and resets the dirty flag.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    connection._commit()
+    set_clean(using=using)
+
+def rollback(using=None):
+    """
+    This function does the rollback itself and resets the dirty flag.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    connection._rollback()
+    set_clean(using=using)
+
+def savepoint(using=None):
+    """
+    Creates a savepoint (if supported and required by the backend) inside the
+    current transaction. Returns an identifier for the savepoint that will be
+    used for the subsequent rollback or commit.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    thread_ident = thread.get_ident()
+    if thread_ident in savepoint_state and using in savepoint_state[thread_ident]:
+        savepoint_state[thread_ident][using].append(None)
+    else:
+        savepoint_state.setdefault(thread_ident, {})
+        savepoint_state[thread_ident][using] = [None]
+    tid = str(thread_ident).replace('-', '')
+    sid = "s%s_x%d" % (tid, len(savepoint_state[thread_ident][using]))
+    connection._savepoint(sid)
+    return sid
+
+def savepoint_rollback(sid, using=None):
+    """
+    Rolls back the most recent savepoint (if one exists). Does nothing if
+    savepoints are not supported.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    thread_ident = thread.get_ident()
+    if thread_ident in savepoint_state and using in savepoint_state[thread_ident]:
+        connection._savepoint_rollback(sid)
+
+def savepoint_commit(sid, using=None):
+    """
+    Commits the most recent savepoint (if one exists). Does nothing if
+    savepoints are not supported.
+    """
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    connection = connections[using]
+    thread_ident = thread.get_ident()
+    if thread_ident in savepoint_state and using in savepoint_state[thread_ident]:
+        connection._savepoint_commit(sid)
+
+##############
+# DECORATORS #
+##############
+
+def autocommit(using=None):
+    """
+    Decorator that activates commit on save. This is Django's default behavior;
+    this decorator is useful if you globally activated transaction management in
+    your settings file and want the default behavior in some view functions.
+    """
+    def inner_autocommit(func, db=None):
+        def _autocommit(*args, **kw):
+            try:
+                enter_transaction_management(managed=False, using=db)
+                managed(False, using=db)
+                return func(*args, **kw)
+            finally:
+                leave_transaction_management(using=db)
+        return wraps(func)(_autocommit)
+
+    # Note that although the first argument is *called* `using`, it
+    # may actually be a function; @autocommit and @autocommit('foo')
+    # are both allowed forms.
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    if callable(using):
+        return inner_autocommit(using, DEFAULT_DB_ALIAS)
+    return lambda func: inner_autocommit(func,  using)
+
+
+def commit_on_success(using=None):
+    """
+    This decorator activates commit on response. This way, if the view function
+    runs successfully, a commit is made; if the viewfunc produces an exception,
+    a rollback is made. This is one of the most common ways to do transaction
+    control in web apps.
+    """
+    def inner_commit_on_success(func, db=None):
+        def _commit_on_success(*args, **kw):
+            try:
+                enter_transaction_management(using=db)
+                managed(True, using=db)
+                try:
+                    res = func(*args, **kw)
+                except:
+                    # All exceptions must be handled here (even string ones).
+                    if is_dirty(using=db):
+                        rollback(using=db)
+                    raise
+                else:
+                    if is_dirty(using=db):
+                        try:
+                            commit(using=db)
+                        except:
+                            rollback(using=db)
+                            raise
+                return res
+            finally:
+                leave_transaction_management(using=db)
+        return wraps(func)(_commit_on_success)
+
+    # Note that although the first argument is *called* `using`, it
+    # may actually be a function; @autocommit and @autocommit('foo')
+    # are both allowed forms.
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    if callable(using):
+        return inner_commit_on_success(using, DEFAULT_DB_ALIAS)
+    return lambda func: inner_commit_on_success(func, using)
+
+def commit_manually(using=None):
+    """
+    Decorator that activates manual transaction control. It just disables
+    automatic transaction control and doesn't do any commit/rollback of its
+    own -- it's up to the user to call the commit and rollback functions
+    themselves.
+    """
+    def inner_commit_manually(func, db=None):
+        def _commit_manually(*args, **kw):
+            try:
+                enter_transaction_management(using=db)
+                managed(True, using=db)
+                return func(*args, **kw)
+            finally:
+                leave_transaction_management(using=db)
+
+        return wraps(func)(_commit_manually)
+
+    # Note that although the first argument is *called* `using`, it
+    # may actually be a function; @autocommit and @autocommit('foo')
+    # are both allowed forms.
+    if using is None:
+        using = DEFAULT_DB_ALIAS
+    if callable(using):
+        return inner_commit_manually(using, DEFAULT_DB_ALIAS)
+    return lambda func: inner_commit_manually(func, using)