[Zope-Checkins] CVS: Zope3/lib/python/Transaction - Exceptions.py:1.2 IDataManager.py:1.2 ITransaction.py:1.2 __init__.py:1.2 _defaultTransaction.py:1.2
Jim Fulton
jim@zope.com
Mon, 10 Jun 2002 19:28:44 -0400
Update of /cvs-repository/Zope3/lib/python/Transaction
In directory cvs.zope.org:/tmp/cvs-serv17445/lib/python/Transaction
Added Files:
Exceptions.py IDataManager.py ITransaction.py __init__.py
_defaultTransaction.py
Log Message:
Merged Zope-3x-branch into newly forked Zope3 CVS Tree.
=== Zope3/lib/python/Transaction/Exceptions.py 1.1 => 1.2 ===
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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.
+ """
=== Zope3/lib/python/Transaction/IDataManager.py 1.1 => 1.2 ===
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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.
+#
+##############################################################################
+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.
+ """
+
+ def abort(object, transaction):
+ """Abort changes made to an object in a transaction"""
+
+ def tpc_begin(transaction, subtransaction=0):
+ """Begin two-phase commit of a transaction
+
+ If a non-zero subtransaction flag is provided, then begin a
+ sub-transaction.
+ """
+
+ def commit(object, transaction):
+ """Commit (tentatively) changes made to an object in a transaction
+
+ This method is called during the first stage of a two-phase commit
+ """
+
+ def tpc_vote(transaction):
+ """Promise to commit a transaction
+
+ This is the last chance to fail. A data manager should have
+ all changes saved in a recoverable fashion.
+
+ Finishes the first phase of a two-phase commit.
+ """
+
+ def tpc_finish(transaction):
+ """Finish the transaction by permanently saving any tentative changes.
+
+ This *must not fail*.
+ """
+
+ def tpc_abort(transaction):
+ """Abort (rollback) any tentative commits performed in the transaction
+ """
+
+ # XXX subtransaction model is pretty primitive.
+ def abort_sub(transaction):
+ """Abort any sub-transaction changes"""
+
+
+ def commit_sub(transaction):
+ """Commit (tentatively) subtransaction changes
+
+ This method is called during the first stage of a two-phase commit
+ """
+
=== Zope3/lib/python/Transaction/ITransaction.py 1.1 => 1.2 ===
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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.
+#
+##############################################################################
+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
+ """
=== Zope3/lib/python/Transaction/__init__.py 1.1 => 1.2 ===
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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
=== Zope3/lib/python/Transaction/_defaultTransaction.py 1.1 => 1.2 ===
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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$
+"""
+import sys
+
+from Transaction.Exceptions import ConflictError, TransactionError
+from zLOG import LOG, ERROR, PANIC
+
+# 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 = []
+
+ def _init(self):
+ self._objects = []
+ 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 "Transaction %s %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 = info.split("\t")
+ self.user = info[0].strip()
+ self.description = ("\t".join(info[1:])).strip()
+
+ 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 (i in jars):
+ 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 (i in jars):
+ jars[i]=j
+ j.commit_sub(self)
+
+ jarsv = jars.values()
+ for jar in jarsv:
+ if not subtransaction:
+ jar.tpc_vote(self)
+
+ 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("Transaction", 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("Transaction", 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("Transaction", 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._objects.append(object)
+
+ def note(self, text):
+ if self.description:
+ self.description = "%s\n\n%s" % (self.description, text.strip())
+ else:
+ self.description = text.strip()
+
+ def setUser(self, user_name, path='/'):
+ self.user = "%s %s" % (path, user_name)
+
+ def setExtendedInfo(self, name, value):
+ if self._extension is None:
+ self._extension = {}
+ self._extension[name] = value
+
+
+############################################################################
+# install get_transaction:
+
+try:
+ import thread
+except:
+ _t = Transaction(None)
+
+ def get_transaction():
+ return _t
+
+ def free_transaction():
+ _t.__init__()
+
+else:
+ _t = {}
+ def get_transaction():
+ id = thread.get_ident()
+ t = _t.get(id)
+ if t is None:
+ _t[id] = t = Transaction(id)
+ return t
+
+ def free_transaction():
+ id = thread.get_ident()
+ try:
+ del _t[id]
+ except KeyError:
+ pass
+
+__all__ = ["Transaction", "get_transaction", "free_transaction"]