[Zope-Checkins] CVS: Zope3/lib/python/Transaction - Exceptions.py:1.1.2.1 IDataManager.py:1.1.2.1 ITransaction.py:1.1.2.1 __init__.py:1.1.2.1 _defaultTransaction.py:1.1.2.1

Jim Fulton jim@zope.com
Sun, 25 Nov 2001 19:16:06 -0500


Update of /cvs-repository/Zope3/lib/python/Transaction
In directory cvs.zope.org:/tmp/cvs-serv13352

Added Files:
      Tag: Zope-3x-branch
	Exceptions.py IDataManager.py ITransaction.py __init__.py 
	_defaultTransaction.py 
Log Message:
Factored transactions into separate package.


=== Added File Zope3/lib/python/Transaction/Exceptions.py ===
##############################################################################
# Copyright (c) 2001 Zope Corporation and Contributors.  All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 
# FOR A PARTICULAR PURPOSE.
##############################################################################

class TransactionError(Exception):
    """An error occured due to normal transaction processing
    """

class ConflictError(TransactionError):
    """Two transactions tried to modify the same object at once

    This transaction should be resubmitted.
    """


=== Added File Zope3/lib/python/Transaction/IDataManager.py ===
##############################################################################
# Copyright (c) 2001 Zope Corporation and Contributors.  All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 
# FOR A PARTICULAR PURPOSE.
##############################################################################

# Hack to overcome absense of Interface package
try:
    from Interface import Interface
except ImportError:
    class Interface: pass


class IDataManager(Interface):
    """Data management interface for storing objects transactionally

    This is currently implemented by ZODB database connections.
    """

    # This needs to be filled out. :)

    



=== Added File Zope3/lib/python/Transaction/ITransaction.py ===
##############################################################################
# Copyright (c) 2001 Zope Corporation and Contributors.  All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 
# FOR A PARTICULAR PURPOSE.
##############################################################################

# Hack to overcome absense of Interface package
try:
    from Interface import Interface
except ImportError:
    class Interface: pass

class ITransaction(Interface):
    """Transaction objects

    Application code typically gets these by calling
    get_transaction().
    """

    def abort(subtransaction=0):
        """Abort the current transaction

        If subtransaction is true, then only abort the current subtransaction.
        """

    def begin(info=None, subtransaction=0):
        """Begin a transaction

        If info is specified, it must be a string and will be used to
        initialize the transaction description.

        If subtransaction is true, then begin subtransaction.
        """

    def commit(subtransaction=0):
        """Commit a transaction

        If subtransaction is true, then only abort the current subtransaction.
        """

    def register(object):
        """Register the object with the current transaction.

        The object may have a '_p_jar' attribute. If it has this
        attribute then the attribute value may be 'None', or an object
        that implements the IDataManager interface. If the value is
        'None', then the object will not affect the current
        transaction.

        If the object doesn't have a '_p_jar' attribute, then the
        object must implement the IDataManager interface itself.
        """

    def note(text):
        """Add the text to the transaction description

        If there previous description isn't empty, a blank line is
        added before the new text.
        """

    def setUser(user_name, path="/"):
        """Set the transaction user name.

        The user is actually set to the path followed by a space and
        the user name.
        """

    def setExtendedInfo(name, value):
        """Set extended information
        """


=== Added File Zope3/lib/python/Transaction/__init__.py ===
##############################################################################
# Copyright (c) 2001 Zope Corporation and Contributors.  All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 
# FOR A PARTICULAR PURPOSE.
##############################################################################
def get_transaction():
    return get_transaction_hook()

from _defaultTransaction import get_transaction as get_transaction_hook

from _defaultTransaction import Transaction


=== Added File Zope3/lib/python/Transaction/_defaultTransaction.py ===
##############################################################################
# Copyright (c) 2001 Zope Corporation and Contributors.  All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 1.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 
# FOR A PARTICULAR PURPOSE.
##############################################################################
"""Transaction management

$Id: _defaultTransaction.py,v 1.1.2.1 2001/11/26 00:16:06 jim Exp $"""
__version__='$Revision: 1.1.2.1 $'[11:-2]

import time, sys, struct
from struct import pack
from string import split, strip, join
from zLOG import LOG, ERROR, PANIC

from Exceptions import ConflictError, TransactionError

# Flag indicating whether certain errors have occurred.
hosed=0

class Transaction:
    'Simple transaction objects for single-threaded applications.'
    user=''
    description=''
    _connections=None
    _extension=None
    _sub=None # This is a subtrasaction flag

    # The _non_st_objects variable is either None or a list
    # of jars that do not support subtransactions. This is used to
    # manage non-subtransaction-supporting jars during subtransaction
    # commits and aborts to ensure that they are correctly committed
    # or aborted in the "outside" transaction.
    _non_st_objects=None
    
    def __init__(self, id=None):
        self._id=id
        self._objects=[]
        self._append=self._objects.append

    def _init(self):
        self._objects=[]
        self._append=self._objects.append
        self.user=self.description=''
        if self._connections:
            for c in self._connections.values(): c.close()
            del self._connections

    def sub(self):
        # Create a manually managed subtransaction for internal use
        r=self.__class__()
        r.user=self.user
        r.description=self.description
        r._extension=self._extension
        return r
        
    def __str__(self): return "%.3f\t%s" % (self._id or 0, self.user)

    def __del__(self):
        if self._objects: self.abort(freeme=0)

    def abort(self, subtransaction=0, freeme=1):
        '''Abort the transaction.

        This is called from the application.  This means that we haven\'t
        entered two-phase commit yet, so no tpc_ messages are sent.
        '''

        if subtransaction and (self._non_st_objects is not None):
            raise TransactionError, (
                """Attempted to abort a sub-transaction, but a participating
                data manager doesn't support partial abort.
                """)

        t = None
        subj = self._sub
        subjars = ()

        if not subtransaction:

            # Must add in any non-subtransaction supporting objects that
            # may have been stowed away from previous subtransaction
            # commits.
            if self._non_st_objects is not None:
                self._objects.extend(self._non_st_objects)
                self._non_st_objects = None

            if subj is not None:
                # Abort of top-level transaction after commiting
                # subtransactions.
                subjars = subj.values()
                self._sub = None

        try:
            # Abort the objects
            for o in self._objects:
                try:
                    j = getattr(o, '_p_jar', o)
                    if j is not None:
                        j.abort(o, self)
                except:
                    if t is None:
                        t, v, tb = sys.exc_info()

            # Ugh, we need to abort work done in sub-transactions.
            while subjars:
                j = subjars.pop()
                j.abort_sub(self) # This should never fail
        
            if t is not None:
                raise t, v, tb

        finally:
            if t is not None:
                del tb # don't keep traceback in local variable
            del self._objects[:] # Clear registered
            if not subtransaction and freeme:
                if self._id is not None:
                    free_transaction()
            else:
                self._init()

    def begin(self, info=None, subtransaction=None):
        '''Begin a new transaction.

        This aborts any transaction in progres.
        '''
        if self._objects: self.abort(subtransaction, 0)
        if info:
            info=split(info,'\t')
            self.user=strip(info[0])
            self.description=strip(join(info[1:],'\t'))

    def commit(self, subtransaction=None):
        'Finalize the transaction'

        global hosed
        
        objects=self._objects
        jars={}
        jarsv = None
        subj=self._sub
        subjars=()

        if subtransaction:
            if subj is None: self._sub=subj={}
        else:
            if subj is not None:
                if objects:
                    # Do an implicit sub-transaction commit:
                    self.commit(1)
                    objects=[]
                subjars=subj.values()
                self._sub=None

        # If not a subtransaction, then we need to add any non-
        # subtransaction-supporting objects that may have been
        # stowed away during subtransaction commits to _objects.
        if (subtransaction is None) and (self._non_st_objects is not None):
            append=objects.append
            for object in self._non_st_objects:
                append(object)
            self._non_st_objects = None

        t=v=tb=None

        if (objects or subjars) and hosed:
            # Something really bad happened and we don't
            # trust the system state.
            raise TransactionError, (
                
                """A serious error, which was probably a system error,
                occurred in a previous database transaction.  This
                application may be in an invalid state and must be
                restarted before database updates can be allowed.

                Beware though that if the error was due to a serious
                system problem, such as a disk full condition, then
                the application may not come up until you deal with
                the system problem.  See your application log for
                information on the error that lead to this problem.
                """)

        try:

            # It's important that:
            #
            # - Every object in self._objects is either committed
            #   or aborted.
            #
            # - For each object that is committed
            #   we call tpc_begin on it's jar at least once
            #
            # - For every jar for which we've called tpc_begin on,
            #   we either call tpc_abort or tpc_finish. It is OK
            #   to call these multiple times, as the storage is
            #   required to ignore these calls if tpc_begin has not
            #   been called.
            
            ncommitted=0
            try:
                for o in objects:
                    j=getattr(o, '_p_jar', o)
                    if j is not None:
                        i=id(j)
                        if not jars.has_key(i):
                            jars[i]=j
                            if subtransaction:

                                # If a jar does not support subtransactions,
                                # we need to save it away to be committed in
                                # the outer transaction.
                                try: j.tpc_begin(self, subtransaction)
                                except TypeError:
                                    j.tpc_begin(self)

                                if hasattr(j, 'commit_sub'):
                                    subj[i]=j
                                else:
                                    if self._non_st_objects is None:
                                        self._non_st_objects = []
                                    self._non_st_objects.append(o)
                                    continue

                            else:
                                j.tpc_begin(self)
                        j.commit(o,self)
                    ncommitted=ncommitted+1

                # Commit work done in subtransactions
                while subjars:
                    j=subjars.pop()
                    i=id(j)
                    if not jars.has_key(i):
                        jars[i]=j
                    
                    j.commit_sub(self)

                jarsv = jars.values()
                for jar in jarsv:
                    if not subtransaction:
                        try: jar=jar.tpc_vote
                        except: pass
                        else: jar(self) # last chance to bail

                try:
                    # Try to finish one jar, since we may be able to
                    # recover if the first one fails.
                    if jarsv:
                        jarsv[-1].tpc_finish(self) # This should never fail
                        jarsv.pop() # It didn't, so it's taken care of.
                except:
                    # Bug if it does, we need to keep track of it
                    LOG('ZODB', ERROR,
                        "A storage error occurred in the last phase of a "
                        "two-phase commit.  This shouldn\'t happen. ",
                        error=sys.exc_info())
                    raise

                try:
                    while jarsv:
                        jarsv[-1].tpc_finish(self) # This should never fail
                        jarsv.pop() # It didn't, so it's taken care of.
                except:                        
                    # Bug if it does, we need to yell FIRE!
                    # Someone finished, so don't allow any more
                    # work without at least a restart!
                    hosed=1
                    LOG('ZODB', PANIC,
                        "A storage error occurred in the last phase of a "
                        "two-phase commit.  This shouldn\'t happen. "
                        "The application may be in a hosed state, so "
                        "transactions will not be allowed to commit "
                        "until the site/storage is reset by a restart. ",
                        error=sys.exc_info())
                    raise
                
            except:
                t,v,tb=sys.exc_info()

                # Ugh, we got an got an error during commit, so we
                # have to clean up.

                # First, we have to abort any uncommitted objects.
                for o in objects[ncommitted:]:
                    try:
                        j=getattr(o, '_p_jar', o)
                        if j is not None: j.abort(o, self)
                    except: pass

                # Then, we unwind TPC for the jars that began it.
                if jarsv is None:
                    jarsv = jars.values()
                for j in jarsv:
                    try: j.tpc_abort(self) # This should never fail
                    except:     
                        LOG('ZODB', ERROR,
                            "A storage error occured during object abort "
                            "This shouldn\'t happen. ",
                            error=sys.exc_info())
                
                # Ugh, we need to abort work done in sub-transactions.
                while subjars:
                    j=subjars.pop()
                    j.abort_sub(self) # This should never fail

                raise t,v,tb

        finally:
            tb=None
            del objects[:] # clear registered
            if not subtransaction and self._id is not None: free_transaction()

    def register(self,object):
        'Register the given object for transaction control.'
        self._append(object)

    def note(self, text):
        if self.description:
            self.description = "%s\n\n%s" % (self.description, strip(text))
        else: 
            self.description = strip(text)
    
    def setUser(self, user_name, path='/'):
        self.user="%s %s" % (path, user_name)

    def setExtendedInfo(self, name, value):
        ext=self._extension
        if ext is None:
            ext=self._extension={}
        ext[name]=value


############################################################################
# install get_transaction:

try:
    import thread

except:
    _t=Transaction(None)
    def get_transaction(_t=_t): return _t
    def free_transaction(_t=_t): _t.__init__()

else:
    _t={}
    def get_transaction(_id=thread.get_ident, _t=_t, get=_t.get, None=None):
        id=_id()
        t=get(id, None)
        if t is None: _t[id]=t=Transaction(id)
        return t

    def free_transaction(_id=thread.get_ident, _t=_t):
        id=_id()
        try: del _t[id]
        except KeyError: pass

    del thread

del _t