[Zodb-checkins] SVN: ZODB/branches/3.4/src/ Added savepoints!

Jim Fulton jim at zope.com
Sat Apr 23 21:29:02 EDT 2005


Log message for revision 30129:
  Added savepoints!
  
  (And also added interfaces and rearranged some code to hopefully make
  it easier to read.)
  

Changed:
  U   ZODB/branches/3.4/src/ZODB/Connection.py
  D   ZODB/branches/3.4/src/ZODB/TmpStore.py
  U   ZODB/branches/3.4/src/ZODB/tests/testConnection.py
  A   ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py
  A   ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.txt
  U   ZODB/branches/3.4/src/ZODB/tests/testZODB.py
  U   ZODB/branches/3.4/src/ZODB/tests/test_datamanageradapter.py
  U   ZODB/branches/3.4/src/transaction/__init__.py
  U   ZODB/branches/3.4/src/transaction/_manager.py
  U   ZODB/branches/3.4/src/transaction/_transaction.py
  U   ZODB/branches/3.4/src/transaction/interfaces.py
  A   ZODB/branches/3.4/src/transaction/savepoint.txt
  A   ZODB/branches/3.4/src/transaction/tests/savepointsample.py
  U   ZODB/branches/3.4/src/transaction/tests/test_register_compat.py
  A   ZODB/branches/3.4/src/transaction/tests/test_savepoint.py

-=-
Modified: ZODB/branches/3.4/src/ZODB/Connection.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/Connection.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/ZODB/Connection.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -17,6 +17,7 @@
 
 import logging
 import sys
+import tempfile
 import threading
 import warnings
 from time import time
@@ -33,13 +34,12 @@
 
 from ZODB.ConflictResolution import ResolvedSerial
 from ZODB.ExportImport import ExportImport
-from ZODB.POSException \
-     import ConflictError, ReadConflictError, InvalidObjectReference, \
-            ConnectionStateError
-from ZODB.TmpStore import TmpStore
+from ZODB import POSException
+from ZODB.POSException import InvalidObjectReference, ConnectionStateError
+from ZODB.POSException import ConflictError, ReadConflictError
 from ZODB.serialize import ObjectWriter, ObjectReader, myhasattr
-from ZODB.utils import u64, oid_repr, z64, positive_id, \
-        DEPRECATED_ARGUMENT, deprecated36
+from ZODB.utils import DEPRECATED_ARGUMENT, deprecated36
+from ZODB.utils import p64, u64, z64, oid_repr, positive_id
 
 global_reset_counter = 0
 
@@ -61,17 +61,19 @@
 
     implements(IConnection, IDataManager, IPersistentDataManager)
 
+    _storage = _normal_storage = _savepoint_storage = None
+
     _tmp = None
     _code_timestamp = 0
 
-    # ZODB.IConnection
+    ##########################################################################
+    # Connection methods, ZODB.IConnection
 
     def __init__(self, version='', cache_size=400,
                  cache_deactivate_after=None, mvcc=True, txn_mgr=None,
                  synch=True):
         """Create a new Connection."""
         self._log = logging.getLogger("ZODB.Connection")
-        self._storage = None
         self._debug_info = ()
         self._opened = None # time.time() when DB.open() opened us
 
@@ -150,39 +152,6 @@
 
         self.connections = None
 
-    def get_connection(self, database_name):
-        """Return a Connection for the named database."""
-        connection = self.connections.get(database_name)
-        if connection is None:
-            new_con = self._db.databases[database_name].open()
-            self.connections.update(new_con.connections)
-            new_con.connections = self.connections
-            connection = new_con
-        return connection
-
-    def get(self, oid):
-        """Return the persistent object with oid 'oid'."""
-        if self._storage is None:
-            raise ConnectionStateError("The database connection is closed")
-
-        obj = self._cache.get(oid, None)
-        if obj is not None:
-            return obj
-        obj = self._added.get(oid, None)
-        if obj is not None:
-            return obj
-
-        p, serial = self._storage.load(oid, self._version)
-        obj = self._reader.getGhost(p)
-
-        obj._p_oid = oid
-        obj._p_jar = self
-        obj._p_changed = None
-        obj._p_serial = serial
-
-        self._cache[oid] = obj
-        return obj
-
     def add(self, obj):
         """Add a new object 'obj' to the database and assign it an oid."""
         if self._storage is None:
@@ -207,52 +176,41 @@
         elif obj._p_jar is not self:
             raise InvalidObjectReference(obj, obj._p_jar)
 
-    def sortKey(self):
-        """Return a consistent sort key for this connection."""
-        return "%s:%s" % (self._storage.sortKey(), id(self))
+    def get(self, oid):
+        """Return the persistent object with oid 'oid'."""
+        if self._storage is None:
+            raise ConnectionStateError("The database connection is closed")
 
-    def abort(self, transaction):
-        """Abort a transaction and forget all changes."""
-        for obj in self._registered_objects:
-            oid = obj._p_oid
-            assert oid is not None
-            if oid in self._added:
-                del self._added[oid]
-                del obj._p_jar
-                del obj._p_oid
-            else:
+        obj = self._cache.get(oid, None)
+        if obj is not None:
+            return obj
+        obj = self._added.get(oid, None)
+        if obj is not None:
+            return obj
 
-                # Note: If we invalidate a non-ghostifiable object
-                # (i.e. a persistent class), the object will
-                # immediately reread it's state.  That means that the
-                # following call could result in a call to
-                # self.setstate, which, of course, must suceed.
-                # In general, it would be better if the read could be
-                # delayed until the start of the next transaction.  If
-                # we read at the end of a transaction and if the
-                # object was invalidated during this transaction, then
-                # we'll read non-current data, which we'll discard
-                # later in transaction finalization.  Unfortnately, we
-                # can only delay the read if this abort corresponds to
-                # a top-level-transaction abort.  We can't tell if
-                # this is a top-level-transaction abort, so we have to
-                # go ahead and invalidate now. Fortunately, it's
-                # pretty unlikely that the object we are invalidating
-                # was invalidated by another thread, so the risk of a
-                # reread is pretty low.
+        p, serial = self._storage.load(oid, self._version)
+        obj = self._reader.getGhost(p)
 
-                self._cache.invalidate(oid)
+        obj._p_oid = oid
+        obj._p_jar = self
+        obj._p_changed = None
+        obj._p_serial = serial
 
-        self._tpc_cleanup()
+        self._cache[oid] = obj
+        return obj
 
+    def cacheMinimize(self, dt=DEPRECATED_ARGUMENT):
+        """Deactivate all unmodified objects in the cache."""
+        if dt is not DEPRECATED_ARGUMENT:
+            deprecated36("cacheMinimize() dt= is ignored.")
+        self._cache.minimize()
+
     # TODO: we should test what happens when cacheGC is called mid-transaction.
-
     def cacheGC(self):
         """Reduce cache size to target size."""
         self._cache.incrgc()
 
     __onCloseCallbacks = None
-
     def onCloseCallback(self, f):
         """Register a callable, f, to be called by close()."""
         if self.__onCloseCallbacks is None:
@@ -266,12 +224,6 @@
             raise ConnectionStateError("Cannot close a connection joined to "
                                        "a transaction")
 
-        if self._tmp is not None:
-            # There are no direct modifications pending, but a subtransaction
-            # is pending.
-            raise ConnectionStateError("Cannot close a connection with a "
-                                       "pending subtransaction")
-
         if self._cache is not None:
             self._cache.incrgc() # This is a good time to do some GC
 
@@ -285,7 +237,8 @@
                     self._log.error("Close callback failed for %s", f,
                                     exc_info=sys.exc_info())
             self.__onCloseCallbacks = None
-        self._storage = self._tmp = self.new_oid = None
+        self._storage = self._savepoint_storage = self._normal_storage = None
+        self.new_oid = None
         self._debug_info = ()
         self._opened = None
         # Return the connection to the pool.
@@ -297,10 +250,192 @@
             # assert that here, because self may have been reused (by
             # another thread) by the time we get back here.
 
-    # transaction.interfaces.IDataManager
+    def db(self):
+        """Returns a handle to the database this connection belongs to."""
+        return self._db
 
+    def isReadOnly(self):
+        """Returns True if the storage for this connection is read only."""
+        if self._storage is None:
+            raise ConnectionStateError("The database connection is closed")
+        return self._storage.isReadOnly()
+
+    def invalidate(self, tid, oids):
+        """Notify the Connection that transaction 'tid' invalidated oids."""
+        self._inv_lock.acquire()
+        try:
+            if self._txn_time is None:
+                self._txn_time = tid
+            self._invalidated.update(oids)
+        finally:
+            self._inv_lock.release()
+
+    def root(self):
+        """Return the database root object."""
+        return self.get(z64)
+
+    def getVersion(self):
+        """Returns the version this connection is attached to."""
+        if self._storage is None:
+            raise ConnectionStateError("The database connection is closed")
+        return self._version
+
+    def get_connection(self, database_name):
+        """Return a Connection for the named database."""
+        connection = self.connections.get(database_name)
+        if connection is None:
+            new_con = self._db.databases[database_name].open()
+            self.connections.update(new_con.connections)
+            new_con.connections = self.connections
+            connection = new_con
+        return connection
+
+    def sync(self):
+        """Manually update the view on the database."""
+        self._txn_mgr.get().abort()
+        sync = getattr(self._storage, 'sync', 0)
+        if sync:
+            sync()
+        self._flush_invalidations()
+
+    def getDebugInfo(self):
+        """Returns a tuple with different items for debugging the
+        connection.
+        """
+        return self._debug_info
+
+    def setDebugInfo(self, *args):
+        """Add the given items to the debug information of this connection."""
+        self._debug_info = self._debug_info + args
+
+    def getTransferCounts(self, clear=False):
+        """Returns the number of objects loaded and stored."""
+        res = self._load_count, self._store_count
+        if clear:
+            self._load_count = 0
+            self._store_count = 0
+        return res
+
+    # Connection methods
+    ##########################################################################
+
+    ##########################################################################
+    # Data manager (IDataManager) methods
+
+    def abort(self, transaction):
+        """Abort a transaction and forget all changes."""
+
+        if self._savepoint_storage is not None:
+            self._abort_savepoint()
+
+        self._abort()
+
+        self._tpc_cleanup()
+
+    def _abort(self):
+        """Abort a transaction and forget all changes."""
+        
+        for obj in self._registered_objects:
+            oid = obj._p_oid
+            assert oid is not None
+            if oid in self._added:
+                del self._added[oid]
+                del obj._p_jar
+                del obj._p_oid
+            else:
+
+                # Note: If we invalidate a non-ghostifiable object
+                # (i.e. a persistent class), the object will
+                # immediately reread it's state.  That means that the
+                # following call could result in a call to
+                # self.setstate, which, of course, must suceed.
+                # In general, it would be better if the read could be
+                # delayed until the start of the next transaction.  If
+                # we read at the end of a transaction and if the
+                # object was invalidated during this transaction, then
+                # we'll read non-current data, which we'll discard
+                # later in transaction finalization.  Unfortnately, we
+                # can only delay the read if this abort corresponds to
+                # a top-level-transaction abort.  We can't tell if
+                # this is a top-level-transaction abort, so we have to
+                # go ahead and invalidate now. Fortunately, it's
+                # pretty unlikely that the object we are invalidating
+                # was invalidated by another thread, so the risk of a
+                # reread is pretty low.
+
+                self._cache.invalidate(oid)
+
+    def _tpc_cleanup(self):
+        """Performs cleanup operations to support tpc_finish and tpc_abort."""
+        self._conflicts.clear()
+        if not self._synch:
+            self._flush_invalidations()
+        self._needs_to_join = True
+        self._registered_objects = []
+
+    def _flush_invalidations(self):
+        self._inv_lock.acquire()
+        try:
+
+            # Non-ghostifiable objects may need to read when they are
+            # invalidated, so, we'll quickly just replace the
+            # invalidating dict with a new one.  We'll then process
+            # the invalidations after freeing the lock *and* after
+            # reseting the time.  This means that invalidations will
+            # happen after the start of the transactions.  They are
+            # subject to conflict errors and to reading old data,
+
+            # TODO: There is a potential problem lurking for persistent
+            # classes.  Suppose we have an invlidation of a persistent
+            # class and of an instance.  If the instance is
+            # invalidated first and if the invalidation logic uses
+            # data read from the class, then the invalidation could
+            # be performed with state data.  Or, suppose that there
+            # are instances of the class that are freed as a result of
+            # invalidating some object.  Perhaps code in their __del__
+            # uses class data.  Really, the only way to properly fix
+            # this is to, in fact, make classes ghostifiable.  Then
+            # we'd have to reimplement attribute lookup to check the
+            # class state and, if necessary, activate the class.  It's
+            # much worse than that though, because we'd also need to
+            # deal with slots.  When a class is ghostified, we'd need
+            # to replace all of the slot operations with versions that
+            # reloaded the object when caled. It's hard to say which
+            # is better for worse.  For now, it seems the risk of
+            # using a class while objects are being invalidated seems
+            # small enough t be acceptable.
+
+            invalidated = self._invalidated
+            self._invalidated = {}
+            self._txn_time = None
+        finally:
+            self._inv_lock.release()
+
+        self._cache.invalidate(invalidated)
+
+        # Now is a good time to collect some garbage.
+        self._cache.incrgc()
+
+    def tpc_begin(self, transaction):
+        """Begin commit of a transaction, starting the two-phase commit."""
+        self._modified = []
+
+        # _creating is a list of oids of new objects, which is used to
+        # remove them from the cache if a transaction aborts.
+        self._creating = []
+        self._normal_storage.tpc_begin(transaction)
+
     def commit(self, transaction):
         """Commit changes to an object"""
+
+        if self._savepoint_storage is not None:
+            self._commit_savepoint(transaction)
+
+        self._commit(transaction)
+
+    def _commit(self, transaction):
+        """Commit changes to an object"""
+
         if self._import:
             # TODO:  This code seems important for Zope, but needs docs
             # to explain why.
@@ -377,169 +512,6 @@
 
             self._handle_serial(s, oid)
 
-    def commit_sub(self, t):
-        """Commit all changes made in subtransactions and begin 2-phase commit
-        """
-        if self._tmp is None:
-            return
-        src = self._storage
-        self._storage = self._tmp
-        self._tmp = None
-
-        self._log.debug("Commiting subtransaction of size %s", src.getSize())
-        oids = src._index.keys()
-        self._storage.tpc_begin(t)
-
-        # Copy invalidating and creating info from temporary storage:
-        self._modified.extend(oids)
-        self._creating.extend(src._creating)
-
-        for oid in oids:
-            data, serial = src.load(oid, src)
-            s = self._storage.store(oid, serial, data, self._version, t)
-            self._handle_serial(s, oid, change=False)
-
-    def abort_sub(self, t):
-        """Discard all subtransaction data."""
-        if self._tmp is None:
-            return
-        src = self._storage
-        self._storage = self._tmp
-        self._tmp = None
-
-        # Note: If we invalidate a non-ghostifiable object (i.e. a
-        # persistent class), the object will immediately reread it's
-        # state.  That means that the following call could result in a
-        # call to self.setstate, which, of course, must succeed.  In
-        # general, it would be better if the read could be delayed
-        # until the start of the next transaction.  If we read at the
-        # end of a transaction and if the object was invalidated
-        # during this transaction, then we'll read non-current data,
-        # which we'll discard later in transaction finalization.  We
-        # could, theoretically queue this invalidation by calling
-        # self.invalidate.  Unfortunately, attempts to make that
-        # change resulted in mysterious test failures.  It's pretty
-        # unlikely that the object we are invalidating was invalidated
-        # by another thread, so the risk of a reread is pretty low.
-        # It's really not worth the effort to pursue this.
-
-        self._cache.invalidate(src._index.keys())
-        self._invalidate_creating(src._creating)
-
-    def _invalidate_creating(self, creating=None):
-        """Disown any objects newly saved in an uncommitted transaction."""
-        if creating is None:
-            creating = self._creating
-            self._creating = []
-
-        for oid in creating:
-            o = self._cache.get(oid)
-            if o is not None:
-                del self._cache[oid]
-                del o._p_jar
-                del o._p_oid
-
-    # The next two methods are callbacks for transaction synchronization.
-
-    def beforeCompletion(self, txn):
-        # We don't do anything before a commit starts.
-        pass
-
-    def afterCompletion(self, txn):
-        self._flush_invalidations()
-
-    def _flush_invalidations(self):
-        self._inv_lock.acquire()
-        try:
-
-            # Non-ghostifiable objects may need to read when they are
-            # invalidated, so, we'll quickly just replace the
-            # invalidating dict with a new one.  We'll then process
-            # the invalidations after freeing the lock *and* after
-            # reseting the time.  This means that invalidations will
-            # happen after the start of the transactions.  They are
-            # subject to conflict errors and to reading old data,
-
-            # TODO: There is a potential problem lurking for persistent
-            # classes.  Suppose we have an invlidation of a persistent
-            # class and of an instance.  If the instance is
-            # invalidated first and if the invalidation logic uses
-            # data read from the class, then the invalidation could
-            # be performed with state data.  Or, suppose that there
-            # are instances of the class that are freed as a result of
-            # invalidating some object.  Perhaps code in their __del__
-            # uses class data.  Really, the only way to properly fix
-            # this is to, in fact, make classes ghostifiable.  Then
-            # we'd have to reimplement attribute lookup to check the
-            # class state and, if necessary, activate the class.  It's
-            # much worse than that though, because we'd also need to
-            # deal with slots.  When a class is ghostified, we'd need
-            # to replace all of the slot operations with versions that
-            # reloaded the object when caled. It's hard to say which
-            # is better for worse.  For now, it seems the risk of
-            # using a class while objects are being invalidated seems
-            # small enough t be acceptable.
-
-            invalidated = self._invalidated
-            self._invalidated = {}
-            self._txn_time = None
-        finally:
-            self._inv_lock.release()
-
-        self._cache.invalidate(invalidated)
-
-        # Now is a good time to collect some garbage.
-        self._cache.incrgc()
-
-    def root(self):
-        """Return the database root object."""
-        return self.get(z64)
-
-    def db(self):
-        """Returns a handle to the database this connection belongs to."""
-        return self._db
-
-    def isReadOnly(self):
-        """Returns True if the storage for this connection is read only."""
-        if self._storage is None:
-            raise ConnectionStateError("The database connection is closed")
-        return self._storage.isReadOnly()
-
-    def invalidate(self, tid, oids):
-        """Notify the Connection that transaction 'tid' invalidated oids."""
-        self._inv_lock.acquire()
-        try:
-            if self._txn_time is None:
-                self._txn_time = tid
-            self._invalidated.update(oids)
-        finally:
-            self._inv_lock.release()
-
-    # IDataManager
-
-    def tpc_begin(self, transaction, sub=False):
-        """Begin commit of a transaction, starting the two-phase commit."""
-        self._modified = []
-
-        # _creating is a list of oids of new objects, which is used to
-        # remove them from the cache if a transaction aborts.
-        self._creating = []
-        if sub and self._tmp is None:
-            # Sub-transaction!
-            self._tmp = self._storage
-            self._storage = TmpStore(self._version, self._storage)
-
-        self._storage.tpc_begin(transaction)
-
-    def tpc_vote(self, transaction):
-        """Verify that a data manager can commit the transaction."""
-        try:
-            vote = self._storage.tpc_vote
-        except AttributeError:
-            return
-        s = vote(transaction)
-        self._handle_serial(s)
-
     def _handle_serial(self, store_return, oid=None, change=1):
         """Handle the returns from store() and tpc_vote() calls."""
 
@@ -582,26 +554,13 @@
                 obj._p_changed = 0 # transition from changed to up-to-date
             obj._p_serial = serial
 
-    def tpc_finish(self, transaction):
-        """Indicate confirmation that the transaction is done."""
-        if self._tmp is not None:
-            # Commiting a subtransaction!
-            # There is no need to invalidate anything.
-            self._storage.tpc_finish(transaction)
-            self._storage._creating[:0]=self._creating
-            del self._creating[:]
-        else:
-            def callback(tid):
-                d = {}
-                for oid in self._modified:
-                    d[oid] = 1
-                self._db.invalidate(tid, d, self)
-            self._storage.tpc_finish(transaction, callback)
-        self._tpc_cleanup()
-
     def tpc_abort(self, transaction):
         if self._import:
             self._import = None
+
+        if self._savepoint_storage is not None:
+            self._abort_savepoint()
+            
         self._storage.tpc_abort(transaction)
 
         # Note: If we invalidate a non-justifiable object (i.e. a
@@ -628,41 +587,59 @@
             del obj._p_jar
         self._tpc_cleanup()
 
-    def _tpc_cleanup(self):
-        """Performs cleanup operations to support tpc_finish and tpc_abort."""
-        self._conflicts.clear()
-        if not self._synch:
-            self._flush_invalidations()
-        self._needs_to_join = True
-        self._registered_objects = []
+    def _invalidate_creating(self, creating=None):
+        """Disown any objects newly saved in an uncommitted transaction."""
+        if creating is None:
+            creating = self._creating
+            self._creating = []
 
-    def sync(self):
-        """Manually update the view on the database."""
-        self._txn_mgr.get().abort()
-        sync = getattr(self._storage, 'sync', 0)
-        if sync:
-            sync()
-        self._flush_invalidations()
+        for oid in creating:
+            o = self._cache.get(oid)
+            if o is not None:
+                del self._cache[oid]
+                del o._p_jar
+                del o._p_oid
 
-    def getDebugInfo(self):
-        """Returns a tuple with different items for debugging the
-        connection.
-        """
-        return self._debug_info
+    def tpc_vote(self, transaction):
+        """Verify that a data manager can commit the transaction."""
+        try:
+            vote = self._storage.tpc_vote
+        except AttributeError:
+            return
+        s = vote(transaction)
+        self._handle_serial(s)
 
-    def setDebugInfo(self, *args):
-        """Add the given items to the debug information of this connection."""
-        self._debug_info = self._debug_info + args
+    def tpc_finish(self, transaction):
+        """Indicate confirmation that the transaction is done."""
+        def callback(tid):
+            d = {}
+            for oid in self._modified:
+                d[oid] = 1
+            self._db.invalidate(tid, d, self)
+        self._storage.tpc_finish(transaction, callback)
+        self._tpc_cleanup()
 
-    def getTransferCounts(self, clear=False):
-        """Returns the number of objects loaded and stored."""
-        res = self._load_count, self._store_count
-        if clear:
-            self._load_count = 0
-            self._store_count = 0
-        return res
+    def sortKey(self):
+        """Return a consistent sort key for this connection."""
+        return "%s:%s" % (self._storage.sortKey(), id(self))
 
-    ##############################################
+    # Data manager (IDataManager) methods
+    ##########################################################################
+
+    ##########################################################################
+    # Transaction-manager synchronization -- ISynchronizer
+
+    def beforeCompletion(self, txn):
+        # We don't do anything before a commit starts.
+        pass
+
+    def afterCompletion(self, txn):
+        self._flush_invalidations()
+
+    # Transaction-manager synchronization -- ISynchronizer
+    ##########################################################################
+    
+    ##########################################################################
     # persistent.interfaces.IPersistentDatamanager
 
     def oldstate(self, obj, tid):
@@ -818,12 +795,24 @@
         self._register(obj)
 
     def _register(self, obj=None):
-        if obj is not None:
-            self._registered_objects.append(obj)
+
+        # The order here is important.  We need to join before
+        # registering the object, because joining may take a
+        # savepoint, and the savepoint should not reflect the change
+        # to the object.
+        
         if self._needs_to_join:
             self._txn_mgr.get().join(self)
             self._needs_to_join = False
 
+        if obj is not None:
+            self._registered_objects.append(obj)
+
+    
+    # persistent.interfaces.IPersistentDatamanager
+    ##########################################################################
+
+    ##########################################################################
     # PROTECTED stuff (used by e.g. ZODB.DB.DB)
 
     def _cache_items(self):
@@ -862,7 +851,7 @@
         # and Storage.
 
         self._db = odb
-        self._storage = odb._storage
+        self._normal_storage = self._storage = odb._storage
         self.new_oid = odb._storage.new_oid
         self._opened = time()
         if synch is not None:
@@ -892,6 +881,7 @@
         cache_size = self._cache.cache_size
         self._cache = cache = PickleCache(self, cache_size)
 
+    ##########################################################################
     # Python protocol
 
     def __repr__(self):
@@ -901,6 +891,10 @@
             ver = ''
         return '<Connection at %08x%s>' % (positive_id(self), ver)
 
+    # Python protocol
+    ##########################################################################
+
+    ##########################################################################
     # DEPRECATION candidates
 
     __getitem__ = get
@@ -916,33 +910,6 @@
         except KeyError:
             return self.getVersion()
 
-    def getVersion(self):
-        """Returns the version this connection is attached to."""
-        if self._storage is None:
-            raise ConnectionStateError("The database connection is closed")
-        return self._version
-
-    def setklassstate(self, obj):
-        # Special case code to handle ZClasses, I think.
-        # Called the cache when an object of type type is invalidated.
-        try:
-            oid = obj._p_oid
-            p, serial = self._storage.load(oid, self._version)
-
-            # We call getGhost(), but we actually get a non-ghost back.
-            # The object is a class, which can't actually be ghosted.
-            copy = self._reader.getGhost(p)
-            obj.__dict__.clear()
-            obj.__dict__.update(copy.__dict__)
-
-            obj._p_oid = oid
-            obj._p_jar = self
-            obj._p_changed = 0
-            obj._p_serial = serial
-        except:
-            self._log.error("setklassstate failed", exc_info=sys.exc_info())
-            raise
-
     def exchange(self, old, new):
         # called by a ZClasses method that isn't executed by the test suite
         oid = old._p_oid
@@ -952,8 +919,20 @@
         self._register(new)
         self._cache[oid] = new
 
+    # DEPRECATION candidates
+    ##########################################################################
+
+    ##########################################################################
     # DEPRECATED methods
 
+    def cacheFullSweep(self, dt=None):
+        deprecated36("cacheFullSweep is deprecated. "
+                     "Use cacheMinimize instead.")
+        if dt is None:
+            self._cache.full_sweep()
+        else:
+            self._cache.full_sweep(dt)
+
     def getTransaction(self):
         """Get the current transaction for this connection.
 
@@ -986,16 +965,150 @@
                 self._txn_mgr.registerSynch(self)
         return self._txn_mgr
 
-    def cacheFullSweep(self, dt=None):
-        deprecated36("cacheFullSweep is deprecated. "
-                     "Use cacheMinimize instead.")
-        if dt is None:
-            self._cache.full_sweep()
-        else:
-            self._cache.full_sweep(dt)
+    # DEPRECATED methods
+    ##########################################################################
 
-    def cacheMinimize(self, dt=DEPRECATED_ARGUMENT):
-        """Deactivate all unmodified objects in the cache."""
-        if dt is not DEPRECATED_ARGUMENT:
-            deprecated36("cacheMinimize() dt= is ignored.")
-        self._cache.minimize()
+    #####################################################################
+    # Savepoint support
+
+    def savepoint(self):
+        if self._savepoint_storage is None:
+            self._savepoint_storage = TmpStore(self._version,
+                                               self._normal_storage)
+            self._storage = self._savepoint_storage
+
+        self._creating = []
+        self._commit(None)
+        self._storage.creating.extend(self._creating)
+        del self._creating[:]
+        self._registered_objects = []
+
+        state = self._storage.position, self._storage.index.copy()
+        return Savepoint(self, state)
+
+    def _rollback(self, state):
+        self._abort()
+        src = self._storage
+        self._cache.invalidate(src.index)
+        src.reset(*state)
+
+    def _commit_savepoint(self, transaction):
+        """Commit all changes made in subtransactions and begin 2-phase commit
+        """
+        src = self._savepoint_storage
+        self._storage = self._normal_storage
+        self._savepoint_storage = None
+
+        self._log.debug("Commiting savepoints of size %s", src.getSize())
+        oids = src.index.keys()
+
+        # Copy invalidating and creating info from temporary storage:
+        self._modified.extend(oids)
+        self._creating.extend(src.creating)
+
+        for oid in oids:
+            data, serial = src.load(oid, src)
+            s = self._storage.store(oid, serial, data,
+                                    self._version, transaction)
+            self._handle_serial(s, oid, change=False)
+
+        src.close()
+
+    def _abort_savepoint(self):
+        """Discard all subtransaction data."""
+        src = self._savepoint_storage
+        self._storage = self._normal_storage
+        self._savepoint_storage = None
+
+        # Note: If we invalidate a non-ghostifiable object (i.e. a
+        # persistent class), the object will immediately reread it's
+        # state.  That means that the following call could result in a
+        # call to self.setstate, which, of course, must succeed.  In
+        # general, it would be better if the read could be delayed
+        # until the start of the next transaction.  If we read at the
+        # end of a transaction and if the object was invalidated
+        # during this transaction, then we'll read non-current data,
+        # which we'll discard later in transaction finalization.  We
+        # could, theoretically queue this invalidation by calling
+        # self.invalidate.  Unfortunately, attempts to make that
+        # change resulted in mysterious test failures.  It's pretty
+        # unlikely that the object we are invalidating was invalidated
+        # by another thread, so the risk of a reread is pretty low.
+        # It's really not worth the effort to pursue this.
+
+        self._cache.invalidate(src.index)
+        self._invalidate_creating(src.creating)
+        src.close()
+
+    # Savepoint support
+    #####################################################################
+
+class Savepoint:
+
+    def __init__(self, datamanager, state):
+        self.datamanager = datamanager
+        self.state = state
+
+    def rollback(self):
+        self.datamanager._rollback(self.state)
+
+class TmpStore:
+    """A storage-like thing to support savepoints."""
+
+    def __init__(self, base_version, storage):
+        self._storage = storage
+        for method in (
+            'getName', 'new_oid', 'modifiedInVersion', 'getSize', 
+            'undoLog', 'versionEmpty', 'sortKey',
+            ):
+            setattr(self, method, getattr(storage, method))
+            
+        self._base_version = base_version
+        self._file = tempfile.TemporaryFile()
+        # position: current file position
+        # _tpos: file position at last commit point
+        self.position = 0L
+        # index: map oid to pos of last committed version
+        self.index = {}
+        self.creating = []
+                    
+    def __len__(self):
+        return len(self.index)
+
+    def close(self):
+        self._file.close()
+
+    def load(self, oid, version):
+        pos = self.index.get(oid)
+        if pos is None:
+            return self._storage.load(oid, self._base_version)
+        self._file.seek(pos)
+        h = self._file.read(8)
+        oidlen = u64(h)
+        read_oid = self._file.read(oidlen)
+        if read_oid != oid:
+            raise POSException.StorageSystemError('Bad temporary storage')
+        h = self._file.read(16)
+        size = u64(h[8:])
+        serial = h[:8]
+        return self._file.read(size), serial
+
+    def store(self, oid, serial, data, version, transaction):
+        # we have this funny signature so we can reuse the normal non-commit
+        # commit logic
+        assert version == self._base_version
+        self._file.seek(self.position)
+        l = len(data)
+        if serial is None:
+            serial = z64
+        header = p64(len(oid)) + oid + serial + p64(l)
+        self._file.write(header)
+        self._file.write(data)
+        self.index[oid] = self.position
+        self.position += l + len(header)
+        return serial
+
+    def reset(self, position, index):
+        self._file.truncate(position)
+        self.position = position
+        self.index = index

Deleted: ZODB/branches/3.4/src/ZODB/TmpStore.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/TmpStore.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/ZODB/TmpStore.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -1,126 +0,0 @@
-##############################################################################
-#
-# 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.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
-#
-##############################################################################
-
-from ZODB import POSException
-from ZODB.utils import p64, u64, z64
-
-import tempfile
-
-class TmpStore:
-    """A storage to support subtransactions."""
-
-    _bver = ''
-
-    def __init__(self, base_version, storage):
-        self._transaction = None
-        self._storage = storage
-        if base_version:
-            self._bver = base_version
-        self._file = tempfile.TemporaryFile()
-        # _pos: current file position
-        # _tpos: file position at last commit point
-        self._pos = self._tpos = 0L
-        # _index: map oid to pos of last committed version
-        self._index = {}
-        # _tindex: map oid to pos for new updates
-        self._tindex = {}
-        self._creating = []
-
-    def close(self):
-        self._file.close()
-
-    def getName(self):
-        return self._storage.getName()
-
-    def getSize(self):
-        return self._pos
-
-    def load(self, oid, version):
-        pos = self._index.get(oid)
-        if pos is None:
-            return self._storage.load(oid, self._bver)
-        self._file.seek(pos)
-        h = self._file.read(8)
-        oidlen = u64(h)
-        read_oid = self._file.read(oidlen)
-        if read_oid != oid:
-            raise POSException.StorageSystemError('Bad temporary storage')
-        h = self._file.read(16)
-        size = u64(h[8:])
-        serial = h[:8]
-        return self._file.read(size), serial
-
-    def sortKey(self):
-        return self._storage.sortKey()
-
-    # TODO: clarify difference between self._storage & self._db._storage
-
-    def modifiedInVersion(self, oid):
-        if self._index.has_key(oid):
-            return self._bver
-        return self._storage.modifiedInVersion(oid)
-
-    def new_oid(self):
-        return self._storage.new_oid()
-
-    def registerDB(self, db, limit):
-        pass
-
-    def store(self, oid, serial, data, version, transaction):
-        if transaction is not self._transaction:
-            raise POSException.StorageTransactionError(self, transaction)
-        self._file.seek(self._pos)
-        l = len(data)
-        if serial is None:
-            serial = z64
-        header = p64(len(oid)) + oid + serial + p64(l)
-        self._file.write(header)
-        self._file.write(data)
-        self._tindex[oid] = self._pos
-        self._pos += l + len(header)
-        return serial
-
-    def tpc_abort(self, transaction):
-        if transaction is not self._transaction:
-            return
-        self._tindex.clear()
-        self._transaction = None
-        self._pos = self._tpos
-
-    def tpc_begin(self, transaction):
-        if self._transaction is transaction:
-            return
-        self._transaction = transaction
-        self._tindex.clear() # Just to be sure!
-        self._pos = self._tpos
-
-    def tpc_vote(self, transaction):
-        pass
-
-    def tpc_finish(self, transaction, f=None):
-        if transaction is not self._transaction:
-            return
-        if f is not None:
-            f()
-        self._index.update(self._tindex)
-        self._tindex.clear()
-        self._tpos = self._pos
-
-    def undoLog(self, first, last, filter=None):
-        return ()
-
-    def versionEmpty(self, version):
-        # TODO: what is this supposed to do?
-        if version == self._bver:
-            return len(self._index)

Modified: ZODB/branches/3.4/src/ZODB/tests/testConnection.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/tests/testConnection.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/ZODB/tests/testConnection.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -294,7 +294,7 @@
         >>> cn.close()  # this was succeeding
         Traceback (most recent call last):
           ...
-        ConnectionStateError: Cannot close a connection with a pending subtransaction
+        ConnectionStateError: Cannot close a connection joined to a transaction
 
         Again this leaves the connection as it was.
         >>> transaction.commit()

Added: ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -0,0 +1,29 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+"""Tests of savepoint feature
+
+$Id$
+"""
+import unittest
+from zope.testing import doctest
+
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite('testConnectionSavepoint.txt'),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+


Property changes on: ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.txt
===================================================================
--- ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.txt	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.txt	2005-04-24 01:29:02 UTC (rev 30129)
@@ -0,0 +1,137 @@
+Savepoints
+==========
+
+Savepoints provide a way to save to disk intermediate work done during
+a transaction allowing:
+
+- partial transaction (subtransaction) rollback (abort)
+
+- state of saved objects to be freed, freeing on-line memory for other
+  uses
+
+Savepoints make it possible to write atomic subroutines that don't
+make top-level transaction commitments.
+
+Applications
+------------
+
+To demonstrate how savepoints work with transactions, we'll show an example.
+
+    >>> import ZODB.tests.util
+    >>> db = ZODB.tests.util.DB()
+    >>> connection = db.open()
+    >>> root = connection.root()
+    >>> root['name'] = 'bob'
+
+As with other data managers, we can commit changes:
+
+    >>> import transaction
+    >>> transaction.commit()
+    >>> root['name']
+    'bob'
+
+and abort changes:
+
+    >>> root['name'] = 'sally'
+    >>> root['name']
+    'sally'
+    >>> transaction.abort()
+    >>> root['name']
+    'bob'
+
+Now, lets look at an application that manages funds for people.
+It allows deposits and debits to be entered for multiple people.
+It accepts a sequence of entries and generates a sequence of status
+messages.  For each entry, it applies the change and then validates
+the user's account.  If the user's account is invalid, we role back
+the change for that entry.  The success or failure of an entry is 
+indicated in the output status. First we'll initialize some accounts:
+
+    >>> root['bob-balance'] = 0.0
+    >>> root['bob-credit'] = 0.0
+    >>> root['sally-balance'] = 0.0
+    >>> root['sally-credit'] = 100.0
+    >>> transaction.commit()
+
+Now, we'll define a validation function to validate an account:
+
+    >>> def validate_account(name):
+    ...     if root[name+'-balance'] + root[name+'-credit'] < 0:
+    ...         raise ValueError('Overdrawn', name)
+
+And a function to apply entries.  If the function fails in some
+unexpected way, it rolls back all of it's changes and 
+prints the error:
+
+    >>> def apply_entries(entries):
+    ...     savepoint = transaction.savepoint()
+    ...     try:
+    ...         for name, amount in entries:
+    ...             entry_savepoint = transaction.savepoint()
+    ...             try:
+    ...                 root[name+'-balance'] += amount
+    ...                 validate_account(name)
+    ...             except ValueError, error:
+    ...                 entry_savepoint.rollback()
+    ...                 print 'Error', str(error)
+    ...             else:
+    ...                 print 'Updated', name
+    ...     except Exception, error:
+    ...         savepoint.rollback()
+    ...         print 'Unexpected exception', error
+
+Now let's try applying some entries:
+
+    >>> apply_entries([
+    ...     ('bob',   10.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   20.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   -100.0),
+    ...     ('sally', -100.0),
+    ...     ])
+    Updated bob
+    Updated sally
+    Updated bob
+    Updated sally
+    Error ('Overdrawn', 'bob')
+    Updated sally
+
+    >>> root['bob-balance']
+    30.0
+
+    >>> root['sally-balance']
+    -80.0
+    
+If we give provide entries that cause an unexpected error:
+
+    >>> apply_entries([
+    ...     ('bob',   10.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   '20.0'),
+    ...     ('sally', 10.0),
+    ...     ])
+    Updated bob
+    Updated sally
+    Unexpected exception unsupported operand type(s) for +=: 'float' and 'str'
+
+Because the apply_entries used a savepoint for the entire function, 
+it was able to rollback the partial changes without rolling back
+changes made in the previous call to apply_entries:
+
+    >>> root['bob-balance']
+    30.0
+
+    >>> root['sally-balance']
+    -80.0
+
+If we now abort the outer transactions, the earlier changes will go
+away:
+
+    >>> transaction.abort()
+
+    >>> root['bob-balance']
+    0.0
+
+    >>> root['sally-balance']
+    0.0


Property changes on: ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: ZODB/branches/3.4/src/ZODB/tests/testZODB.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/tests/testZODB.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/ZODB/tests/testZODB.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -550,13 +550,17 @@
         self.assertEqual(rt['a'], 1)
 
         rt['b'] = 2
+
         # Subtransactions don't do tpc_vote, so we poison tpc_begin.
-        poisoned = PoisonedObject(PoisonedJar(break_tpc_begin=True))
-        transaction.get().register(poisoned)
+        poisoned = PoisonedJar()
+        transaction.get().join(poisoned)
+        poisoned.break_savepoint = True
         self.assertRaises(PoisonedError, transaction.get().commit, True)
         # Trying to subtxn-commit again fails too.
-        self.assertRaises(TransactionFailedError, transaction.get().commit, True)
-        self.assertRaises(TransactionFailedError, transaction.get().commit, True)
+        self.assertRaises(TransactionFailedError,
+                          transaction.get().commit, True)
+        self.assertRaises(TransactionFailedError,
+                          transaction.get().commit, True)
         # Top-level commit also fails.
         self.assertRaises(TransactionFailedError, transaction.get().commit)
 
@@ -568,6 +572,7 @@
         # also raises TransactionFailedError.
         self.assertRaises(TransactionFailedError, rt.__setitem__, 'b', 2)
 
+
         # Clean up via abort(), and try again.
         transaction.get().abort()
         rt['a'] = 1
@@ -576,13 +581,18 @@
 
         # Cleaning up via begin() should also work.
         rt['a'] = 2
-        transaction.get().register(poisoned)
+        poisoned = PoisonedJar()
+        transaction.get().join(poisoned)
+        poisoned.break_savepoint = True
         self.assertRaises(PoisonedError, transaction.get().commit, True)
-        self.assertRaises(TransactionFailedError, transaction.get().commit, True)
+        self.assertRaises(TransactionFailedError,
+                          transaction.get().commit, True)
+
         # The change to rt['a'] is lost.
         self.assertEqual(rt['a'], 1)
         # Trying to modify an object also fails.
         self.assertRaises(TransactionFailedError, rt.__setitem__, 'b', 2)
+
         # Clean up via begin(), and try again.
         transaction.begin()
         rt['a'] = 2
@@ -603,9 +613,11 @@
 # PoisonedJar arranges to raise exceptions from interesting places.
 # For whatever reason, subtransaction commits don't call tpc_vote.
 class PoisonedJar:
-    def __init__(self, break_tpc_begin=False, break_tpc_vote=False):
+    def __init__(self, break_tpc_begin=False, break_tpc_vote=False,
+                 break_savepoint=False):
         self.break_tpc_begin = break_tpc_begin
         self.break_tpc_vote = break_tpc_vote
+        self.break_savepoint = break_savepoint
 
     def sortKey(self):
         return str(id(self))
@@ -620,14 +632,10 @@
         if self.break_tpc_vote:
             raise PoisonedError("tpc_vote fails")
 
-    # commit_sub is needed else this jar is ignored during subtransaction
-    # commit.
-    def commit_sub(*args):
-        pass
+    def savepoint(self):
+        if self.break_savepoint:
+            raise PoisonedError("savepoint fails")        
 
-    def abort_sub(*args):
-        pass
-
     def commit(*args):
         pass
 

Modified: ZODB/branches/3.4/src/ZODB/tests/test_datamanageradapter.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/tests/test_datamanageradapter.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/ZODB/tests/test_datamanageradapter.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -238,569 +238,6 @@
     False
     """
 
-def test_commit_w_subtransactions():
-    """
-    So, we have a data manager:
-
-    >>> dm = DataManager()
-
-    and we do some work that modifies uncommited state:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    Now we'll commit the changes in a subtransaction.  When the data
-    manager joins a transaction, the transaction will create an
-    adapter.
-
-    >>> dma = DataManagerAdapter(dm)
-
-    and register it as a modified object. At commit time, the
-    transaction will get the "jar" like this:
-
-    >>> jar = getattr(dma, '_p_jar', dma)
-
-    and, of course, the jar and the adapter will be the same:
-
-    >>> jar is dma
-    True
-
-    The transaction will call tpc_begin:
-
-    >>> t1 = '1'
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-
-    Then the transaction will call commit on the jar:
-
-    >>> jar.commit(t1)
-
-    This doesn't actually do anything. :)
-
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    The transaction will then call tpc_vote:
-
-    >>> jar.tpc_vote(t1)
-
-    This doesn't do anything either, because zodb4 data managers don't
-    actually do two-phase commit for subtransactions.
-
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    Finally, we call tpc_finish. This does actally create a savepoint,
-    but we can't really tell that from outside.
-
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    We'll do more of the above:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 2)
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 2)
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 3)
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 3)
-
-    Note that the bove works *because* the same transaction is used
-    for each subtransaction.
-
-    Finally, we'll do a little more work:
-
-    >>> dm.inc()
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 5)
-
-    and then commit the top-level transaction.
-
-    The transaction  will actually go through the steps for a subtransaction:
-
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-
-    And then call commit_sub:
-
-    >>> jar.commit_sub(t1)
-
-    As usual, this doesn't actually do anything. ;)
-
-    >>> dm.state, dm.delta
-    (0, 5)
-
-    The transaction manager doesn's call tpc_begin, because commit_sub
-    implies the start of two-phase commit. Next, it does call commit:
-
-    >>> jar.commit(t1)
-
-    which doesn't do anything.
-
-    Finally, the transaction calls tpc_vote:
-
-    >>> jar.tpc_vote(t1)
-
-    which actually does something (because this is the top-level txn):
-
-    >>> dm.state, dm.delta
-    (5, 5)
-    >>> dm.prepared
-    True
-
-    Finally, tpc_finish is called:
-
-    >>> jar.tpc_finish(t1)
-
-    and the data manager finishes the two-phase commit:
-
-    >>> dm.state, dm.delta
-    (5, 0)
-    >>> dm.prepared
-    False
-    """
-
-def test_commit_w_subtransactions_featuring_subtransaction_abort():
-    """
-    So, we have a data manager:
-
-    >>> dm = DataManager()
-
-    and we do some work that modifies uncommited state:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    Now we'll commit the changes in a subtransaction.  When the data
-    manager joins a transaction, the transaction will create an
-    adapter.
-
-    >>> dma = DataManagerAdapter(dm)
-
-    and register it as a modified object. At commit time, the
-    transaction will get the "jar" like this:
-
-    >>> jar = getattr(dma, '_p_jar', dma)
-
-    and, of course, the jar and the adapter will be the same:
-
-    >>> jar is dma
-    True
-
-    The transaction will call tpc_begin:
-
-    >>> t1 = '1'
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-
-    Then the transaction will call commit on the jar:
-
-    >>> jar.commit(t1)
-
-    This doesn't actually do anything. :)
-
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    The transaction will then call tpc_vote:
-
-    >>> jar.tpc_vote(t1)
-
-    This doesn't do anything either, because zodb4 data managers don't
-    actually do two-phase commit for subtransactions.
-
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    Finally, we call tpc_finish. This does actally create a savepoint,
-    but we can't really tell that from outside.
-
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    We'll do more of the above:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 2)
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 2)
-
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 3)
-
-    But then we'll decide to abort a subtransaction.
-
-    The transaction will just call abort as usual:
-
-    >>> jar.abort(t1)
-
-    This will cause a rollback to the last savepoint:
-    >>> dm.state, dm.delta
-    (0, 2)
-
-    Then we do more work:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 3)
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 3)
-
-    Note that the bove works *because* the same transaction is used
-    for each subtransaction.
-
-    Finally, we'll do a little more work:
-
-    >>> dm.inc()
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 5)
-
-    and then commit the top-level transaction.
-
-    The transaction  will actually go through the steps for a subtransaction:
-
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-
-    And then call commit_sub:
-
-    >>> jar.commit_sub(t1)
-
-    As usual, this doesn't actually do anything. ;)
-
-    >>> dm.state, dm.delta
-    (0, 5)
-
-    The transaction manager doesn's call tpc_begin, because commit_sub
-    implies the start of two-phase commit. Next, it does call commit:
-
-    >>> jar.commit(t1)
-
-    which doesn't do anything.
-
-    Finally, the transaction calls tpc_vote:
-
-    >>> jar.tpc_vote(t1)
-
-    which actually does something (because this is the top-level txn):
-
-    >>> dm.state, dm.delta
-    (5, 5)
-    >>> dm.prepared
-    True
-
-    Finally, tpc_finish is called:
-
-    >>> jar.tpc_finish(t1)
-
-    and the data manager finishes the two-phase commit:
-
-    >>> dm.state, dm.delta
-    (5, 0)
-    >>> dm.prepared
-    False
-    """
-
-def test_abort_w_subtransactions():
-    """
-    So, we have a data manager:
-
-    >>> dm = DataManager()
-
-    and we do some work that modifies uncommited state:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    Now we'll commit the changes in a subtransaction.  When the data
-    manager joins a transaction, the transaction will create an
-    adapter.
-
-    >>> dma = DataManagerAdapter(dm)
-
-    and register it as a modified object. At commit time, the
-    transaction will get the "jar" like this:
-
-    >>> jar = getattr(dma, '_p_jar', dma)
-
-    and, of course, the jar and the adapter will be the same:
-
-    >>> jar is dma
-    True
-
-    The transaction will call tpc_begin:
-
-    >>> t1 = '1'
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-
-    Then the transaction will call commit on the jar:
-
-    >>> jar.commit(t1)
-
-    This doesn't actually do anything. :)
-
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    The transaction will then call tpc_vote:
-
-    >>> jar.tpc_vote(t1)
-
-    This doesn't do anything either, because zodb4 data managers don't
-    actually do two-phase commit for subtransactions.
-
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    Finally, we call tpc_finish. This does actally create a savepoint,
-    but we can't really tell that from outside.
-
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    We'll do more of the above:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 2)
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 2)
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 3)
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 3)
-
-    Note that the bove works *because* the same transaction is used
-    for each subtransaction.
-
-    Finally, we'll do a little more work:
-
-    >>> dm.inc()
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 5)
-
-    and then abort the top-level transaction.
-
-    The transaction first call abort on the jar:
-
-    >>> jar.abort(t1)
-
-    This will have the effect of aborting the subtrancation:
-
-    >>> dm.state, dm.delta
-    (0, 3)
-
-    Then the transaction will call abort_sub:
-
-    >>> jar.abort_sub(t1)
-
-    This will abort all of the subtransactions:
-
-    >>> dm.state, dm.delta
-    (0, 0)
-    """
-
-
-def test_tpc_abort_w_subtransactions_featuring_subtransaction_abort():
-    """
-    So, we have a data manager:
-
-    >>> dm = DataManager()
-
-    and we do some work that modifies uncommited state:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    Now we'll commit the changes in a subtransaction.  When the data
-    manager joins a transaction, the transaction will create an
-    adapter.
-
-    >>> dma = DataManagerAdapter(dm)
-
-    and register it as a modified object. At commit time, the
-    transaction will get the "jar" like this:
-
-    >>> jar = getattr(dma, '_p_jar', dma)
-
-    and, of course, the jar and the adapter will be the same:
-
-    >>> jar is dma
-    True
-
-    The transaction will call tpc_begin:
-
-    >>> t1 = '1'
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-
-    Then the transaction will call commit on the jar:
-
-    >>> jar.commit(t1)
-
-    This doesn't actually do anything. :)
-
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    The transaction will then call tpc_vote:
-
-    >>> jar.tpc_vote(t1)
-
-    This doesn't do anything either, because zodb4 data managers don't
-    actually do two-phase commit for subtransactions.
-
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    Finally, we call tpc_finish. This does actally create a savepoint,
-    but we can't really tell that from outside.
-
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 1)
-
-    We'll do more of the above:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 2)
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 2)
-
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 3)
-
-    But then we'll decide to abort a subtransaction.
-
-    The transaction will just call abort as usual:
-
-    >>> jar.abort(t1)
-
-    This will cause a rollback to the last savepoint:
-    >>> dm.state, dm.delta
-    (0, 2)
-
-    Then we do more work:
-
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 3)
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-    >>> dm.state, dm.delta
-    (0, 3)
-
-    Note that the bove works *because* the same transaction is used
-    for each subtransaction.
-
-    Finally, we'll do a little more work:
-
-    >>> dm.inc()
-    >>> dm.inc()
-    >>> dm.state, dm.delta
-    (0, 5)
-
-    and then commit the top-level transaction.
-
-    The transaction  will actually go through the steps for a subtransaction:
-
-    >>> jar.tpc_begin(t1, 1) # 1 -> subtxn
-    >>> jar.commit(t1)
-    >>> jar.tpc_vote(t1)
-    >>> jar.tpc_finish(t1)
-
-    And then call commit_sub:
-
-    >>> jar.commit_sub(t1)
-
-    As usual, this doesn't actually do anything. ;)
-
-    >>> dm.state, dm.delta
-    (0, 5)
-
-    The transaction manager doesn's call tpc_begin, because commit_sub
-    implies the start of two-phase commit. Next, it does call commit:
-
-    >>> jar.commit(t1)
-
-    which doesn't do anything.
-
-    Finally, the transaction calls tpc_vote:
-
-    >>> jar.tpc_vote(t1)
-
-    which actually does something (because this is the top-level txn):
-
-    >>> dm.state, dm.delta
-    (5, 5)
-    >>> dm.prepared
-    True
-
-    Now, at the last minute, the transaction is aborted (possibly due
-    to a "no vote" from another data manager):
-
-    >>> jar.tpc_abort(t1)
-
-    An the changes are undone:
-
-    >>> dm.state, dm.delta
-    (0, 0)
-    >>> dm.prepared
-    False
-    """
-
 def test_suite():
     return DocTestSuite()
 

Modified: ZODB/branches/3.4/src/transaction/__init__.py
===================================================================
--- ZODB/branches/3.4/src/transaction/__init__.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/transaction/__init__.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -24,6 +24,7 @@
 begin = manager.begin
 commit = manager.commit
 abort = manager.abort
+savepoint = manager.savepoint
 
 def get_transaction():
     from ZODB.utils import deprecated36

Modified: ZODB/branches/3.4/src/transaction/_manager.py
===================================================================
--- ZODB/branches/3.4/src/transaction/_manager.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/transaction/_manager.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -67,6 +67,9 @@
     def abort(self, sub=False):
         self.get().abort(sub)
 
+    def savepoint(self, optimistic=False):
+        return self.get().savepoint(optimistic)
+
 class ThreadTransactionManager(TransactionManager):
     """Thread-aware transaction manager.
 

Modified: ZODB/branches/3.4/src/transaction/_transaction.py
===================================================================
--- ZODB/branches/3.4/src/transaction/_transaction.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/transaction/_transaction.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -30,6 +30,8 @@
 Subtransactions
 ---------------
 
+Note: Suntransactions are deprecated!
+
 A subtransaction applies the transaction notion recursively.  It
 allows a set of modifications within a transaction to be committed or
 aborted as a group.  A subtransaction is a strictly local activity;
@@ -82,7 +84,7 @@
 tpc_begin() on each resource manager before calling commit() on any of
 them.
 
-    1. tpc_begin(txn, subtransaction=False)
+    1. tpc_begin(txn)
     2. commit(txn)
     3. tpc_vote(txn)
     4. tpc_finish(txn)
@@ -90,6 +92,8 @@
 Subtransaction commit
 ---------------------
 
+Note: Subtransactions are deprecated!
+
 When a subtransaction commits, the protocol is different.
 
 1. tpc_begin() is passed a second argument, which indicates that a
@@ -128,8 +132,7 @@
 call abort().
 
 Once uncommitted objects are aborted, tpc_abort() or abort_sub() is
-called on each resource manager.  abort_sub() is called if the
-resource manager was involved in a subtransaction.
+called on each resource manager.
 
 Synchronization
 ---------------
@@ -213,14 +216,6 @@
         self.log = logging.getLogger("txn.%d" % thread.get_ident())
         self.log.debug("new transaction")
 
-        # _sub contains all of the resource managers involved in
-        # subtransactions.  It maps id(a resource manager) to the resource
-        # manager.
-        self._sub = {}
-        # _nonsub contains all the resource managers that do not support
-        # subtransactions that were involved in subtransaction commits.
-        self._nonsub = {}
-
         # If a commit fails, the traceback is saved in _failure_traceback.
         # If another attempt is made to commit, TransactionFailedError is
         # raised, incorporating this traceback.
@@ -231,6 +226,9 @@
         # inefficient for FIFO access of this kind.
         self._before_commit = []
 
+        # Keep track of the last savepoint
+        self._last_savepoint = None
+
     # Raise TransactionFailedError, due to commit()/join()/register()
     # getting called when the current transaction has already suffered
     # a commit failure.
@@ -258,6 +256,34 @@
             resource = DataManagerAdapter(resource)
         self._resources.append(resource)
 
+        if self._last_savepoint is not None:
+            self._last_savepoint.join(resource)
+
+    def savepoint(self, optimistic=False):
+        if self.status is Status.COMMITFAILED:
+            self._prior_commit_failed() # doesn't return, it raises
+
+        try:
+            savepoint = Savepoint(optimistic)
+            for resource in self._resources:
+                savepoint.join(resource)
+        except:
+            self._cleanup(self._resources)
+            self._saveCommitishError() # doesn't return, it raises!
+            
+        if self._last_savepoint is not None:
+            savepoint.previous = self._last_savepoint
+            self._last_savepoint.next = savepoint
+        self._last_savepoint = savepoint
+        return savepoint
+
+    def _invalidate_last_savepoint(self):
+        # Invalidate the last savepoint and any previous
+        # savepoints. This is done on a commit or abort.
+        if self._last_savepoint is not None:
+            self._last_savepoint._invalidate_previous()
+            self._last_savepoint = None
+
     def register(self, obj):
         # The old way of registering transaction participants.
         #
@@ -273,10 +299,7 @@
             raise ValueError("Register with no manager")
         adapter = self._adapters.get(manager)
         if adapter is None:
-            if myhasattr(manager, "commit_sub"):
-                adapter = MultiObjectResourceAdapterSub(manager)
-            else:
-                adapter = MultiObjectResourceAdapter(manager)
+            adapter = MultiObjectResourceAdapter(manager)
             adapter.objects.append(obj)
             self._adapters[manager] = adapter
             self.join(adapter)
@@ -286,67 +309,59 @@
             assert id(obj) not in map(id, adapter.objects)
             adapter.objects.append(obj)
 
-            # In the presence of subtransactions, an existing adapter
-            # might be in _adapters but not in _resources.
-            if adapter not in self._resources:
-                self._resources.append(adapter)
-
     def begin(self):
         from ZODB.utils import deprecated36
 
         deprecated36("Transaction.begin() should no longer be used; use "
                       "the begin() method of a transaction manager.")
-        if (self._resources or
-              self._sub or
-              self._nonsub or
-              self._synchronizers):
+        if (self._resources or self._synchronizers):
             self.abort()
         # Else aborting wouldn't do anything, except if _manager is non-None,
         # in which case it would do nothing besides uselessly free() this
         # transaction.
 
     def commit(self, subtransaction=False):
+
+        self._invalidate_last_savepoint()
+
+        if subtransaction:
+            # TODO depricate subtransactions
+            self.savepoint(1)
+            return
+        
         if self.status is Status.COMMITFAILED:
             self._prior_commit_failed() # doesn't return
 
-        if not subtransaction:
-            self._callBeforeCommitHooks()
+        self._callBeforeCommitHooks()
 
-        if not subtransaction and self._sub and self._resources:
-            # This commit is for a top-level transaction that has
-            # previously committed subtransactions.  Do one last
-            # subtransaction commit to clear out the current objects,
-            # then commit all the subjars.
-            self.commit(True)
+        self._synchronizers.map(lambda s: s.beforeCompletion(self))
+        self.status = Status.COMMITTING
 
-        if not subtransaction:
-            self._synchronizers.map(lambda s: s.beforeCompletion(self))
-            self.status = Status.COMMITTING
-
         try:
-            self._commitResources(subtransaction)
+            self._commitResources()
         except:
-            self.status = Status.COMMITFAILED
-            # Save the traceback for TransactionFailedError.
-            ft = self._failure_traceback = StringIO()
-            t, v, tb = sys.exc_info()
-            # Record how we got into commit().
-            traceback.print_stack(sys._getframe(1), None, ft)
-            # Append the stack entries from here down to the exception.
-            traceback.print_tb(tb, None, ft)
-            # Append the exception type and value.
-            ft.writelines(traceback.format_exception_only(t, v))
-            raise t, v, tb
+            self._saveCommitishError() # This raises!
 
-        if subtransaction:
-            self._resources = []
-        else:
-            self.status = Status.COMMITTED
-            if self._manager:
-                self._manager.free(self)
-            self._synchronizers.map(lambda s: s.afterCompletion(self))
-            self.log.debug("commit")
+        self.status = Status.COMMITTED
+        if self._manager:
+            self._manager.free(self)
+        self._synchronizers.map(lambda s: s.afterCompletion(self))
+        self.log.debug("commit")
 
+    def _saveCommitishError(self):
+        self.status = Status.COMMITFAILED
+        # Save the traceback for TransactionFailedError.
+        ft = self._failure_traceback = StringIO()
+        t, v, tb = sys.exc_info()
+        # Record how we got into commit().
+        traceback.print_stack(sys._getframe(1), None, ft)
+        # Append the stack entries from here down to the exception.
+        traceback.print_tb(tb, None, ft)
+        # Append the exception type and value.
+        ft.writelines(traceback.format_exception_only(t, v))
+        raise t, v, tb
+        
+
     def beforeCommitHook(self, hook, *args, **kws):
         self._before_commit.append((hook, args, kws))
 
@@ -357,31 +372,20 @@
             hook, args, kws = self._before_commit.pop(0)
             hook(*args, **kws)
 
-    def _commitResources(self, subtransaction):
+    def _commitResources(self):
         # Execute the two-phase commit protocol.
 
-        L = self._getResourceManagers(subtransaction)
+        L = list(self._resources)
+        L.sort(rm_cmp)
         try:
             for rm in L:
-                # If you pass subtransaction=True to tpc_begin(), it
-                # will create a temporary storage for the duration of
-                # the transaction.  To signal that the top-level
-                # transaction is committing, you must then call
-                # commit_sub().
-                if not subtransaction and id(rm) in self._sub:
-                    del self._sub[id(rm)]
-                    rm.commit_sub(self)
-                else:
-                    rm.tpc_begin(self, subtransaction)
+                rm.tpc_begin(self)
             for rm in L:
                 rm.commit(self)
                 self.log.debug("commit %r" % rm)
-            if not subtransaction:
-                # Not sure why, but it is intentional that you do not
-                # call tpc_vote() for subtransaction commits.
-                for rm in L:
-                    rm.tpc_vote(self)
-                    self._voted[id(rm)] = True
+            for rm in L:
+                rm.tpc_vote(self)
+                self._voted[id(rm)] = True
 
             try:
                 for rm in L:
@@ -401,8 +405,7 @@
             try:
                 self._cleanup(L)
             finally:
-                if not subtransaction:
-                    self._synchronizers.map(lambda s: s.afterCompletion(self))
+                self._synchronizers.map(lambda s: s.afterCompletion(self))
             raise t, v, tb
 
     def _cleanup(self, L):
@@ -415,68 +418,30 @@
                     self.log.error("Error in abort() on manager %s",
                                    rm, exc_info=sys.exc_info())
         for rm in L:
-            if id(rm) in self._sub:
-                try:
-                    rm.abort_sub(self)
-                except Exception:
-                    self.log.error("Error in abort_sub() on manager %s",
-                                   rm, exc_info=sys.exc_info())
-            else:
-                try:
-                    rm.tpc_abort(self)
-                except Exception:
-                    self.log.error("Error in tpc_abort() on manager %s",
-                                   rm, exc_info=sys.exc_info())
+            try:
+                rm.tpc_abort(self)
+            except Exception:
+                self.log.error("Error in tpc_abort() on manager %s",
+                               rm, exc_info=sys.exc_info())
 
-    def _getResourceManagers(self, subtransaction):
-        L = []
-        if subtransaction:
-            # If we are in a subtransaction, make sure all resource
-            # managers are placed in either _sub or _nonsub.  When
-            # the top-level transaction commits, we need to merge
-            # these back into the resource set.
-
-            # If a data manager doesn't support sub-transactions, we
-            # don't do anything with it now.  (That's somewhat okay,
-            # because subtransactions are mostly just an
-            # optimization.)  Save it until the top-level transaction
-            # commits.
-
-            for rm in self._resources:
-                if myhasattr(rm, "commit_sub"):
-                    self._sub[id(rm)] = rm
-                    L.append(rm)
-                else:
-                    self._nonsub[id(rm)] = rm
-        else:
-            if self._sub or self._nonsub:
-                # Merge all of _sub, _nonsub, and _resources.
-                d = dict(self._sub)
-                d.update(self._nonsub)
-                # TODO: I think _sub and _nonsub are disjoint, and that
-                #       _resources is empty.  If so, we can simplify this code.
-                assert len(d) == len(self._sub) + len(self._nonsub)
-                assert not self._resources
-                for rm in self._resources:
-                    d[id(rm)] = rm
-                L = d.values()
-            else:
-                L = list(self._resources)
-
-        L.sort(rm_cmp)
-        return L
-
     def abort(self, subtransaction=False):
-        if not subtransaction:
-            self._synchronizers.map(lambda s: s.beforeCompletion(self))
 
-        if subtransaction and self._nonsub:
-            from ZODB.POSException import TransactionError
-            raise TransactionError("Resource manager does not support "
-                                   "subtransaction abort")
+        if subtransaction:
+            # TODO deprecate subtransactions
+            if not self._last_savepoint:
+                raise interfaces.InvalidSavepointRollbackError
+            if self._last_savepoint.valid:
+                # We're supposed to be able to call abort(1) multiple
+                # times. Sigh.
+                self._last_savepoint.rollback()
+            return
 
+        self._invalidate_last_savepoint()
+        
+        self._synchronizers.map(lambda s: s.beforeCompletion(self))
+
         tb = None
-        for rm in self._resources + self._nonsub.values():
+        for rm in self._resources:
             try:
                 rm.abort(self)
             except:
@@ -485,21 +450,12 @@
                 self.log.error("Failed to abort resource manager: %s",
                                rm, exc_info=sys.exc_info())
 
-        if not subtransaction:
-            for rm in self._sub.values():
-                try:
-                    rm.abort_sub(self)
-                except:
-                    if tb is None:
-                        t, v, tb = sys.exc_info()
-                    self.log.error("Failed to abort_sub resource manager: %s",
-                                   rm, exc_info=sys.exc_info())
+        if self._manager:
+            self._manager.free(self)
+            
+        self._synchronizers.map(lambda s: s.afterCompletion(self))
 
-        if not subtransaction:
-            if self._manager:
-                self._manager.free(self)
-            self._synchronizers.map(lambda s: s.afterCompletion(self))
-            self.log.debug("abort")
+        self.log.debug("abort")
 
         if tb is not None:
             raise t, v, tb
@@ -539,8 +495,8 @@
     def sortKey(self):
         return self.manager.sortKey()
 
-    def tpc_begin(self, txn, sub=False):
-        self.manager.tpc_begin(txn, sub)
+    def tpc_begin(self, txn):
+        self.manager.tpc_begin(txn)
 
     def tpc_finish(self, txn):
         self.manager.tpc_finish(txn)
@@ -571,25 +527,6 @@
         if tb is not None:
             raise t, v, tb
 
-class MultiObjectResourceAdapterSub(MultiObjectResourceAdapter):
-    """Adapt resource managers that participate in subtransactions."""
-
-    def commit_sub(self, txn):
-        self.manager.commit_sub(txn)
-
-    def abort_sub(self, txn):
-        self.manager.abort_sub(txn)
-
-    def tpc_begin(self, txn, sub=False):
-        self.manager.tpc_begin(txn, sub)
-        self.sub = sub
-
-    def tpc_finish(self, txn):
-        self.manager.tpc_finish(txn)
-        if self.sub:
-            self.objects = []
-
-
 def rm_cmp(rm1, rm2):
     return cmp(rm1.sortKey(), rm2.sortKey())
 
@@ -624,50 +561,82 @@
 
     def __init__(self, datamanager):
         self._datamanager = datamanager
-        self._rollback = None
 
     # TODO: I'm not sure why commit() doesn't do anything
 
     def commit(self, transaction):
+        # We don't do anything here because ZODB4-style data managers
+        # didn't have a separate commit step
         pass
 
     def abort(self, transaction):
-
-        # We need to discard any changes since the last save point, or all
-        # changes
-
-        if self._rollback is None:
-            # No previous savepoint, so just abort
-            self._datamanager.abort(transaction)
-        else:
-            self._rollback()
-
-    def abort_sub(self, transaction):
         self._datamanager.abort(transaction)
 
-    def commit_sub(self, transaction):
-        # Nothing to do wrt data, be we begin 2pc for the top-level
-        # trans
-        self._sub = False
-
-    def tpc_begin(self, transaction, subtransaction=False):
-        self._sub = subtransaction
-
+    def tpc_begin(self, transaction):
+        # We don't do anything here because ZODB4-style data managers
+        # didn't have a separate tpc_begin step
+        pass
+        
     def tpc_abort(self, transaction):
-        if self._sub:
-            self.abort(self, transaction)
-        else:
-            self._datamanager.abort(transaction)
+        self._datamanager.abort(transaction)
 
     def tpc_finish(self, transaction):
-        if self._sub:
-            self._rollback = self._datamanager.savepoint(transaction).rollback
-        else:
-            self._datamanager.commit(transaction)
+        self._datamanager.commit(transaction)
 
     def tpc_vote(self, transaction):
-        if not self._sub:
-            self._datamanager.prepare(transaction)
+        self._datamanager.prepare(transaction)
 
     def sortKey(self):
         return self._datamanager.sortKey()
+
+class Savepoint:
+    """Transaction savepoint
+
+    Transaction savepoints coordinate savepoints for data managers
+    participating in a transaction.
+    
+    """
+    interface.implements(interfaces.ISavepoint)
+
+    def __init__(self, optimistic):
+        self._savepoints = []
+        self.valid = True
+        self.next = self.previous = None
+        self.optimistic = optimistic
+    
+    def join(self, datamanager):
+        try:
+            savepoint = datamanager.savepoint
+        except AttributeError:
+            if not self.optimistic:
+                raise TypeError("Savepoints unsupported", datamanager)
+            savepoint = NoRollbackSavepoint(datamanager)
+        else:
+            savepoint = savepoint()
+                
+        self._savepoints.append(savepoint)
+
+    def rollback(self):
+        if not self.valid:
+            raise interfaces.InvalidSavepointRollbackError
+        self._invalidate_next()
+        for savepoint in self._savepoints:
+            savepoint.rollback()
+
+    def _invalidate_next(self):
+        self.valid = False
+        if self.next is not None:
+            self.next._invalidate_next()
+
+    def _invalidate_previous(self):
+        self.valid = False
+        if self.previous is not None:
+            self.previous._invalidate_previous()
+
+class NoRollbackSavepoint:
+
+    def __init__(self, datamanager):
+        self.datamanager = datamanager
+
+    def rollback(self):
+        raise TypeError("Savepoints unsupported", self.datamanager)

Modified: ZODB/branches/3.4/src/transaction/interfaces.py
===================================================================
--- ZODB/branches/3.4/src/transaction/interfaces.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/transaction/interfaces.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -18,158 +18,47 @@
 
 import zope.interface
 
-class ISynchronizer(zope.interface.Interface):
-    """Objects that participate in the transaction-boundary notification API.
-    """
+class ITransactionManager(zope.interface.Interface):
+    """An object that manages a sequence of transactions
 
-    def beforeCompletion(transaction):
-        """Hook that is called by the transaction at the start of a commit."""
-
-    def afterCompletion(transaction):
-        """Hook that is called by the transaction after completing a commit."""
-
-
-class IDataManager(zope.interface.Interface):
-    """Objects that manage transactional storage.
-
-    These objects may manage data for other objects, or they may manage
-    non-object storages, such as relational databases.
-
-    IDataManagerOriginal is the interface currently provided by ZODB
-    database connections, but the intent is to move to the newer
-    IDataManager.
+    Applications use transaction managers to establish transaction boundaries.
     """
 
-    def abort_sub(transaction):
-        """Discard all subtransaction data.
 
-        See subtransaction.txt
+    def begin():
+        """Begin a new transaction.
 
-        This is called when top-level transactions are aborted.
-
-        No further subtransactions can be started once abort_sub()
-        has been called; this is only used when the transaction is
-        being aborted.
-
-        abort_sub also implies the abort of a 2-phase commit.
-
-        This should never fail.
+        If an existing transaction is in progress, it will be aborted.
         """
 
-    def commit_sub(transaction):
-        """Commit all changes made in subtransactions and begin 2-phase commit
-
-        Data are saved *as if* they are part of the current transaction.
-        That is, they will not be persistent unless the current transaction
-        is committed.
-
-        This is called when the current top-level transaction is committed.
-
-        No further subtransactions can be started once commit_sub()
-        has been called; this is only used when the transaction is
-        being committed.
-
-        This call also implies the beginning of 2-phase commit.
+    def get():
+        """Get the current transaction.
         """
 
-    # Two-phase commit protocol.  These methods are called by the
-    # ITransaction object associated with the transaction being
-    # committed.
-
-    def tpc_begin(transaction, subtransaction=False):
-        """Begin commit of a transaction, starting the two-phase commit.
-
-        transaction is the ITransaction instance associated with the
-        transaction being committed.
-
-        subtransaction is a Boolean flag indicating whether the
-        two-phase commit is being invoked for a subtransaction.
-
-        Important note: Subtransactions are modelled in the sense that
-        when you commit a subtransaction, subsequent commits should be
-        for subtransactions as well.  That is, there must be a
-        commit_sub() call between a tpc_begin() call with the
-        subtransaction flag set to true and a tpc_begin() with the
-        flag set to false.
-
+    def commit():
+        """Commit the current transaction
         """
 
-    def tpc_abort(transaction):
-        """Abort a transaction.
-
-        This is called by a transaction manager to end a two-phase commit on
-        the data manager.
-
-        This is always called after a tpc_begin call.
-
-        transaction is the ITransaction instance associated with the
-        transaction being committed.
-
-        This should never fail.
+    def abort(self):
+        """Abort the current transaction
         """
 
-    def tpc_finish(transaction):
-        """Indicate confirmation that the transaction is done.
+    def registerSynch(synch):
+        """Register an ISynchronizer.
 
-        transaction is the ITransaction instance associated with the
-        transaction being committed.
-
-        This should never fail. If this raises an exception, the
-        database is not expected to maintain consistency; it's a
-        serious error.
-
-        It's important that the storage calls the passed function
-        while it still has its lock.  We don't want another thread
-        to be able to read any updated data until we've had a chance
-        to send an invalidation message to all of the other
-        connections!
+        Synchronizers are notified at the beginning and end of
+        transaction completion.
+        
         """
 
-    def tpc_vote(transaction):
-        """Verify that a data manager can commit the transaction
+    def unregisterSynch(synch):
+        """Unregister an ISynchronizer.
 
-        This is the last chance for a data manager to vote 'no'.  A
-        data manager votes 'no' by raising an exception.
-
-        transaction is the ITransaction instance associated with the
-        transaction being committed.
+        Synchronizers are notified at the beginning and end of
+        transaction completion.
+        
         """
 
-    def commit(transaction):
-        """Commit modifications to registered objects.
-
-        Save the object as part of the data to be made persistent if
-        the transaction commits.
-
-        This includes conflict detection and handling. If no conflicts or
-        errors occur it saves the objects in the storage.
-        """
-
-    def abort(transaction):
-        """Abort a transaction and forget all changes.
-
-        Abort must be called outside of a two-phase commit.
-
-        Abort is called by the transaction manager to abort transactions
-        that are not yet in a two-phase commit.
-        """
-
-    def sortKey():
-        """Return a key to use for ordering registered DataManagers
-
-        ZODB uses a global sort order to prevent deadlock when it commits
-        transactions involving multiple resource managers.  The resource
-        manager must define a sortKey() method that provides a global ordering
-        for resource managers.
-        """
-        # Alternate version:
-        #"""Return a consistent sort key for this connection.
-        #
-        #This allows ordering multiple connections that use the same storage in
-        #a consistent manner. This is unique for the lifetime of a connection,
-        #which is good enough to avoid ZEO deadlocks.
-        #"""
-
 class ITransaction(zope.interface.Interface):
     """Object representing a running transaction.
 
@@ -219,9 +108,17 @@
     def join(datamanager):
         """Add a datamanager to the transaction.
 
+        The if the data manager supports savepoints, it must call this
+        *before* making any changes.  If the transaction has had any
+        savepoints, then it will take a savepoint of the data manager
+        when join is called and this savepoint must reflct the state
+        of the data manager before any changes that caused the data
+        manager to join the transaction.
+
         The datamanager must implement the
         transactions.interfaces.IDataManager interface, and be
         adaptable to ZODB.interfaces.IDataManager.
+        
         """
 
     def note(text):
@@ -293,3 +190,161 @@
     # TODO: deprecate this for 3.6.
     def register(object):
         """Register the given object for transaction control."""
+
+class IDataManager(zope.interface.Interface):
+    """Objects that manage transactional storage.
+
+    These objects may manage data for other objects, or they may manage
+    non-object storages, such as relational databases.
+
+    IDataManagerOriginal is the interface currently provided by ZODB
+    database connections, but the intent is to move to the newer
+    IDataManager.
+
+    Note that when data are modified, data managers should join a
+    transaction so that data can be committed when the user commits
+    the transaction.
+
+    """
+
+    # Two-phase commit protocol.  These methods are called by the
+    # ITransaction object associated with the transaction being
+    # committed.
+
+    def abort(transaction):
+        """Abort a transaction and forget all changes.
+
+        Abort must be called outside of a two-phase commit.
+
+        Abort is called by the transaction manager to abort transactions
+        that are not yet in a two-phase commit.
+        """
+
+    def tpc_begin(transaction):
+        """Begin commit of a transaction, starting the two-phase commit.
+
+        transaction is the ITransaction instance associated with the
+        transaction being committed.
+
+        subtransaction is a Boolean flag indicating whether the
+        two-phase commit is being invoked for a subtransaction.
+
+        Important note: Subtransactions are modelled in the sense that
+        when you commit a subtransaction, subsequent commits should be
+        for subtransactions as well.  That is, there must be a
+        commit_sub() call between a tpc_begin() call with the
+        subtransaction flag set to true and a tpc_begin() with the
+        flag set to false.
+
+        """
+
+    def commit(transaction):
+        """Commit modifications to registered objects.
+
+        Save the object as part of the data to be made persistent if
+        the transaction commits.
+
+        This includes conflict detection and handling. If no conflicts or
+        errors occur it saves the objects in the storage.
+        """
+
+    def tpc_abort(transaction):
+        """Abort a transaction.
+
+        This is called by a transaction manager to end a two-phase commit on
+        the data manager.
+
+        This is always called after a tpc_begin call.
+
+        transaction is the ITransaction instance associated with the
+        transaction being committed.
+
+        This should never fail.
+        """
+
+    def tpc_vote(transaction):
+        """Verify that a data manager can commit the transaction
+
+        This is the last chance for a data manager to vote 'no'.  A
+        data manager votes 'no' by raising an exception.
+
+        transaction is the ITransaction instance associated with the
+        transaction being committed.
+        """
+
+    def tpc_finish(transaction):
+        """Indicate confirmation that the transaction is done.
+
+        transaction is the ITransaction instance associated with the
+        transaction being committed.
+
+        This should never fail. If this raises an exception, the
+        database is not expected to maintain consistency; it's a
+        serious error.
+
+        It's important that the storage calls the passed function
+        while it still has its lock.  We don't want another thread
+        to be able to read any updated data until we've had a chance
+        to send an invalidation message to all of the other
+        connections!
+        """
+
+    def sortKey():
+        """Return a key to use for ordering registered DataManagers
+
+        ZODB uses a global sort order to prevent deadlock when it commits
+        transactions involving multiple resource managers.  The resource
+        manager must define a sortKey() method that provides a global ordering
+        for resource managers.
+        """
+        # Alternate version:
+        #"""Return a consistent sort key for this connection.
+        #
+        #This allows ordering multiple connections that use the same storage in
+        #a consistent manner. This is unique for the lifetime of a connection,
+        #which is good enough to avoid ZEO deadlocks.
+        #"""
+
+class ISavepointDataManager(IDataManager):
+
+    def savepoint():
+        """Return a savepoint (ISavepoint)
+        """
+
+class ISavepoint(zope.interface.Interface):
+    """A transaction savepoint
+    """
+
+    def rollback():
+        """Rollback any work done since the savepoint
+
+        An InvalidSavepointRollbackError is raised if the savepoint
+        isn't valid.
+        
+        """
+
+    valid = zope.interface.Attribute(
+        "Boolean indicating whether the savepoint is valid")
+
+class InvalidSavepointRollbackError(Exception):
+    """Attempt to rollback an invalid savepoint
+
+    A savepoint may be invalid because:
+
+    - The surrounding transaction has committed or aborted
+
+    - An earlier savepoint in the same transaction has been rolled back
+    """
+
+class ISynchronizer(zope.interface.Interface):
+    """Objects that participate in the transaction-boundary notification API.
+    """
+
+    def beforeCompletion(transaction):
+        """Hook that is called by the transaction at the start of a commit.
+        """
+
+    def afterCompletion(transaction):
+        """Hook that is called by the transaction after completing a commit.
+        """
+

Added: ZODB/branches/3.4/src/transaction/savepoint.txt
===================================================================
--- ZODB/branches/3.4/src/transaction/savepoint.txt	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/transaction/savepoint.txt	2005-04-24 01:29:02 UTC (rev 30129)
@@ -0,0 +1,221 @@
+Savepoints
+==========
+
+Savepoints provide a way to save to disk intermediate work done during
+a transaction allowing:
+
+- partial transaction (subtransaction) rollback (abort)
+
+- state of saved objects to be freed, freeing on-line memory for other
+  uses
+
+Savepoints make it possible to write atomic subroutines that don't
+make top-level transaction commitments.
+
+Applications
+------------
+
+To demonstrate how savepoints work with transactions, we've provided a
+sample data manager implementation that provides savepoint support.
+The primary purpose of this data manager is to provide code that can
+be read to understand how savepoints work. The secondary purpose is to
+provide support for demonstrating the correct operation of savepoint
+support within the transaction system.  This data manager is very
+simple.  It provides flat storage of named immutable values, like strings
+and numbers.
+
+     
+    >>> import transaction.tests.savepointsample
+    >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
+    >>> dm['name'] = 'bob'
+
+As with other data managers, we can commit changes:
+
+    >>> transaction.commit()
+    >>> dm['name']
+    'bob'
+
+and abort changes:
+
+    >>> dm['name'] = 'sally'
+    >>> dm['name']
+    'sally'
+    >>> transaction.abort()
+    >>> dm['name']
+    'bob'
+
+Now, lets look at an application that manages funds for people.
+It allows deposits and debits to be entered for multiple people.
+It accepts a sequence of entries and generates a sequence of status
+messages.  For each entry, it applies the change and then validates
+the user's account.  If the user's account is invalid, we role back
+the change for that entry.  The success or failure of an entry is 
+indicated in the output status. First we'll initialize some accounts:
+
+    >>> dm['bob-balance'] = 0.0
+    >>> dm['bob-credit'] = 0.0
+    >>> dm['sally-balance'] = 0.0
+    >>> dm['sally-credit'] = 100.0
+    >>> transaction.commit()
+
+Now, we'll define a validation function to validate an account:
+
+    >>> def validate_account(name):
+    ...     if dm[name+'-balance'] + dm[name+'-credit'] < 0:
+    ...         raise ValueError('Overdrawn', name)
+
+And a function to apply entries.  If the function fails in some
+unexpected way, it rolls back all of it's changes and 
+prints the error:
+
+    >>> def apply_entries(entries):
+    ...     savepoint = transaction.savepoint()
+    ...     try:
+    ...         for name, amount in entries:
+    ...             entry_savepoint = transaction.savepoint()
+    ...             try:
+    ...                 dm[name+'-balance'] += amount
+    ...                 validate_account(name)
+    ...             except ValueError, error:
+    ...                 entry_savepoint.rollback()
+    ...                 print 'Error', str(error)
+    ...             else:
+    ...                 print 'Updated', name
+    ...     except Exception, error:
+    ...         savepoint.rollback()
+    ...         print 'Unexpected exception', error
+
+Now let's try applying some entries:
+
+    >>> apply_entries([
+    ...     ('bob',   10.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   20.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   -100.0),
+    ...     ('sally', -100.0),
+    ...     ])
+    Updated bob
+    Updated sally
+    Updated bob
+    Updated sally
+    Error ('Overdrawn', 'bob')
+    Updated sally
+
+    >>> dm['bob-balance']
+    30.0
+
+    >>> dm['sally-balance']
+    -80.0
+    
+If we give provide entries that cause an unexpected error:
+
+    >>> apply_entries([
+    ...     ('bob',   10.0),
+    ...     ('sally', 10.0),
+    ...     ('bob',   '20.0'),
+    ...     ('sally', 10.0),
+    ...     ])
+    Updated bob
+    Updated sally
+    Unexpected exception unsupported operand type(s) for +=: 'float' and 'str'
+
+Because the apply_entries used a savepoint for the entire function, 
+it was able to rollback the partial changes without rolling back
+changes made in the previous call to apply_entries:
+
+    >>> dm['bob-balance']
+    30.0
+
+    >>> dm['sally-balance']
+    -80.0
+
+If we now abort the outer transactions, the earlier changes will go
+away:
+
+    >>> transaction.abort()
+
+    >>> dm['bob-balance']
+    0.0
+
+    >>> dm['sally-balance']
+    0.0
+
+Savepoint invalidation
+----------------------
+
+Once a savepoint has been used, it can't be used again:
+
+    >>> savepoint = transaction.savepoint()
+    >>> dm['bob-balance'] = 100.0
+    >>> dm['bob-balance']
+    100.0
+
+    >>> savepoint.rollback()
+    >>> dm['bob-balance']
+    0.0
+
+    >>> savepoint.rollback()
+    Traceback (most recent call last):
+    ...
+    InvalidSavepointRollbackError
+
+Using a savepoint also invalidates any savepoints that com eafter it:
+
+    >>> savepoint1 = transaction.savepoint()
+    >>> dm['bob-balance'] = 100.0
+    >>> dm['bob-balance']
+    100.0
+
+    >>> savepoint2 = transaction.savepoint()
+    >>> dm['bob-balance'] = 200.0
+    >>> dm['bob-balance']
+    200.0
+
+    >>> savepoint1.rollback()
+    >>> dm['bob-balance']
+    0.0
+
+    >>> savepoint2.rollback()
+    Traceback (most recent call last):
+    ...
+    InvalidSavepointRollbackError
+
+    >>> transaction.abort()
+
+Databases without savepoint support
+-----------------------------------
+
+Normally it's an error to use savepoints with databases that don't
+support savepoints:
+
+    >>> dm_no_sp = transaction.tests.savepointsample.SampleDataManager()
+    >>> dm_no_sp['name'] = 'bob'
+    >>> transaction.commit()
+    >>> dm_no_sp['name'] = 'sally'
+    >>> savepoint = transaction.savepoint()
+    Traceback (most recent call last):
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'bob'})
+
+    >>> transaction.abort()
+    
+However, a flag can be passed to the transaction savepoint method to
+indicate that databases without savepoint support should be tolderated
+until a savepoint is roled back.  This allows transactions to proceed
+is there are no reasons to roll back:
+
+    >>> dm_no_sp['name'] = 'sally'
+    >>> savepoint = transaction.savepoint(1)
+    >>> dm_no_sp['name'] = 'sue'
+    >>> transaction.commit()
+    >>> dm_no_sp['name']
+    'sue'
+
+    >>> savepoint = transaction.savepoint(1)
+    >>> dm_no_sp['name'] = 'sam'
+    >>> savepoint.rollback()
+    Traceback (most recent call last):
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sam'})
+


Property changes on: ZODB/branches/3.4/src/transaction/savepoint.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: ZODB/branches/3.4/src/transaction/tests/savepointsample.py
===================================================================
--- ZODB/branches/3.4/src/transaction/tests/savepointsample.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/transaction/tests/savepointsample.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -0,0 +1,174 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+"""Savepoint data manager implementation example
+
+Sample data manager implementation that illustrates how to implement savepoints
+
+See savepoint.txt in the transaction package.
+
+$Id$
+"""
+
+import UserDict
+from zope import interface
+import transaction.interfaces
+
+class SampleDataManager(UserDict.DictMixin):
+    """Sample implementation of data manager that doesn't support savepoints
+
+    This data manager stores named simple values, like strings and numbers.
+    """
+    
+    interface.implements(transaction.interfaces.ISavepointDataManager)
+
+    def __init__(self, transaction_manager = None):
+        if transaction_manager is None:
+            # Use the thread-local transaction manager if none is provided:
+            transaction_manager = transaction.manager
+        self.transaction_manager = transaction_manager
+
+        # Our committed and uncommitted data:
+        self.committed = {}
+        self.uncommitted = self.committed.copy()
+
+        # Our transaction state:
+
+        #   If our uncommitted data is modified, we'll join a transaction
+        #   and keep track of the transaction we joined.  Any commit
+        #   related messages we get should be for this same transaction
+        self.transaction = None
+
+        #   What phase, if any, of two-phase commit we are in:
+        self.tpc_phase = None
+
+
+    #######################################################################
+    # Provide a mapping interface to uncommitted data.  We provide
+    # a basic subset of the interface. DictMixin does the rest.
+    
+    def __getitem__(self, name):
+        return self.uncommitted[name]
+    
+    def __setitem__(self, name, value):
+        self._join() # join the current transaction, if we haven't already
+        self.uncommitted[name] = value
+    
+    def __delitem__(self, name):
+        self._join() # join the current transaction, if we haven't already
+        del self.uncommitted[name]
+
+    def keys(self):
+        return self.uncommitted.keys()
+
+    #
+    #######################################################################
+
+    #######################################################################
+    # Transaction methods
+
+    def _join(self):
+        # If this is the first change in the transaction, join the transaction
+        if self.transaction is None:
+            self.transaction = self.transaction_manager.get()
+            self.transaction.join(self)
+
+    def _resetTransaction(self):
+        self.transaction = None
+        self.tpc_phase = None
+
+    def abort(self, transaction):
+        """Throw away changes made before the commit process has started
+        """
+        assert ((transaction is self.transaction) or (self.transaction is None)
+                ), "Must not change transactions"
+        assert self.tpc_phase is None, "Must be called outside of tpc"
+        self.uncommitted = self.committed.copy()
+        self._resetTransaction()
+
+    def tpc_begin(self, transaction):
+        """Enter two-phase commit
+        """
+        assert transaction is self.transaction, "Must not change transactions"
+        assert self.tpc_phase is None, "Must be called outside of tpc"
+        self.tpc_phase = 1
+
+    def commit(self, transaction):
+        """Record data modified during the transaction
+        """
+        assert transaction is self.transaction, "Must not change transactions"
+        assert self.tpc_phase == 1, "Must be called in first phase of tpc"
+
+        # In our simple example, we don't need to do anything.
+        # A more complex data manager would typically write to some sort
+        # of log.
+
+    def tpc_vote(self, transaction):
+        assert transaction is self.transaction, "Must not change transactions"
+        assert self.tpc_phase == 1, "Must be called in first phase of tpc"
+        # This particular data manager is always ready to vote.
+        # Real data managers will usually need to take some steps to
+        # make sure that the finish will succeed
+        self.tpc_phase = 2
+
+    def tpc_finish(self, transaction):
+        assert transaction is self.transaction, "Must not change transactions"
+        assert self.tpc_phase == 2, "Must be called in second phase of tpc"
+        self.committed = self.uncommitted.copy()
+        self._resetTransaction()
+        
+    def tpc_abort(self, transaction):
+        assert transaction is self.transaction, "Must not change transactions"
+        assert self.tpc_phase is not None, "Must be called inside of tpc"
+        self.uncommitted = self.committed.copy()
+        self._resetTransaction()
+    
+    #
+    #######################################################################
+
+    #######################################################################
+    # Other data manager methods
+
+    def sortKey(self):
+        # Commit operations on multiple data managers are performed in
+        # sort key order.  This important to avoid deadlock when data
+        # managers are shared among multiple threads or processes and
+        # use locks to manage that sharing.  We aren't going to bother
+        # with that here.
+        return str(id(self))
+
+    #
+    #######################################################################
+
+class SampleSavepointDataManager(SampleDataManager):
+    """Sample implementation of a savepoint-supporting data manager
+
+    This extends the basic data manager with savepoint support.
+    """
+
+    def savepoint(self):
+        # When we create the savepoint, we save the existing database state
+        return SampleSavepoint(self, self.uncommitted.copy())
+
+    def _rollback_savepoint(self, savepoint):
+        # when we rollback the savepoint, we restore the saved data
+        self.uncommitted = savepoint.data
+
+class SampleSavepoint:
+
+    def __init__(self, data_manager, data):
+        self.data_manager = data_manager
+        self.data = data
+
+    def rollback(self):
+        self.data_manager._rollback_savepoint(self)


Property changes on: ZODB/branches/3.4/src/transaction/tests/savepointsample.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Modified: ZODB/branches/3.4/src/transaction/tests/test_register_compat.py
===================================================================
--- ZODB/branches/3.4/src/transaction/tests/test_register_compat.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/transaction/tests/test_register_compat.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -128,7 +128,7 @@
     def sortKey(self):
         return str(id(self))
 
-    def tpc_begin(self, txn, sub):
+    def tpc_begin(self, txn):
         self.calls.append("begin")
 
     def tpc_vote(self, txn):

Added: ZODB/branches/3.4/src/transaction/tests/test_savepoint.py
===================================================================
--- ZODB/branches/3.4/src/transaction/tests/test_savepoint.py	2005-04-24 01:28:59 UTC (rev 30128)
+++ ZODB/branches/3.4/src/transaction/tests/test_savepoint.py	2005-04-24 01:29:02 UTC (rev 30129)
@@ -0,0 +1,29 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+"""Tests of savepoint feature
+
+$Id$
+"""
+import unittest
+from zope.testing import doctest
+
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite('../savepoint.txt'),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+


Property changes on: ZODB/branches/3.4/src/transaction/tests/test_savepoint.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native



More information about the Zodb-checkins mailing list