[Zodb-checkins] SVN: ZODB/trunk/ Merge recent changes (savepoint fixes) from 3.4 branch.

Tim Peters tim.one at comcast.net
Mon Apr 25 14:26:12 EDT 2005


Log message for revision 30169:
  Merge recent changes (savepoint fixes) from 3.4 branch.
  
  Original checkin comments follow:
  
  r30168 | tim_one | 2005-04-25 14:17:37 -0400 (Mon, 25 Apr 2005) | 2 lines
     M /ZODB/branches/3.4/NEWS.txt
     ...
  
  An internal 3.4a5 release, to incorporate savepoint fixes.
  
  r30165 | jim | 2005-04-25 12:29:28 -0400 (Mon, 25 Apr 2005) | 11 lines
     M /ZODB/branches/3.4/src/transaction/_transaction.py
     M /ZODB/branches/3.4/src/transaction/savepoint.txt
     M /ZODB/branches/3.4/src/transaction/tests/test_savepoint.py
  
  Fixed a bug in savepoint rollback.  It's not enough to rollback
  just the savepoint being rolled back because later savepoints
  might involved data managers that hadn't joined when the savepoint
  being rolled back was created.
  
  Now, when a data manager joins and we have savepoints, we create a
  data manager savepoint for the new data manager and add the
  datamanager savepoint to all previous transaction savepoints.  Note
  that this data manager savepoint can be a special savepoint that just
  calls abort on the data manager when it is rolled back.
  
  r30164 | tim_one | 2005-04-25 11:16:20 -0400 (Mon, 25 Apr 2005) | 2 lines
  r30163 | tim_one | 2005-04-25 11:08:37 -0400 (Mon, 25 Apr 2005) | 2 lines
  r30162 | tim_one | 2005-04-25 11:06:51 -0400 (Mon, 25 Apr 2005) | 2 lines
     M /ZODB/branches/3.4/src/transaction/interfaces.py
  
  Grammar, spelling, English.
  Close unterminated sentences.
  Trim trailing whitespace.
  
  r30161 | jim | 2005-04-25 10:51:16 -0400 (Mon, 25 Apr 2005) | 10 lines
     M /ZODB/branches/3.4/src/transaction/interfaces.py
  
  Removed a "self" argument. self is normally not shown in interfaces.
  
  Removed the freeme argument.  This argument is part of the
  implementation, not the public interface.
  
  Removed the subtransaction argument.  Although it is still supported,
  it isn't part of the pblic interface.
  
  Added missing documentation of the savepoint method.
  
  r30160 | jim | 2005-04-25 10:41:08 -0400 (Mon, 25 Apr 2005) | 2 lines
     M /ZODB/branches/3.4/src/transaction/interfaces.py
  
  Removed some stale discussion of subtransactions.
  
  r30147 | jim | 2005-04-24 11:26:39 -0400 (Sun, 24 Apr 2005) | 7 lines
     M /ZODB/branches/3.4/src/transaction/_transaction.py
     M /ZODB/branches/3.4/src/transaction/savepoint.txt
  
  Make transactions uncommitable if savepoint rollback fails.
  
  Added demonstration of transaction non-commitability after savepoint
  or savepoint rollback failure.
  
  Updated "previous commit failed" error to "previous operation failed".
  
  r30146 | jim | 2005-04-24 11:26:37 -0400 (Sun, 24 Apr 2005) | 5 lines
     M /ZODB/branches/3.4/src/ZODB/Connection.py
     M /ZODB/branches/3.4/src/transaction/interfaces.py
     M /ZODB/branches/3.4/src/transaction/tests/savepointsample.py
  
  Refined interfaces to distinguish between data-manager savepoints and
  transaction savepoints.
  
  Updated some interface declarations.
  
  r30145 | jim | 2005-04-24 10:48:15 -0400 (Sun, 24 Apr 2005) | 2 lines
     M /ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py
  
  added explanatory text
  
  r30144 | jim | 2005-04-24 10:35:49 -0400 (Sun, 24 Apr 2005) | 2 lines
  Changed paths:
     M /ZODB/branches/3.4/NEWS.txt
  
  Updated to reflect savepoints.
  
  

Changed:
  U   ZODB/trunk/NEWS.txt
  U   ZODB/trunk/src/ZODB/Connection.py
  U   ZODB/trunk/src/ZODB/tests/testConnectionSavepoint.py
  U   ZODB/trunk/src/transaction/_transaction.py
  U   ZODB/trunk/src/transaction/interfaces.py
  U   ZODB/trunk/src/transaction/savepoint.txt
  U   ZODB/trunk/src/transaction/tests/savepointsample.py
  U   ZODB/trunk/src/transaction/tests/test_savepoint.py

-=-
Modified: ZODB/trunk/NEWS.txt
===================================================================
--- ZODB/trunk/NEWS.txt	2005-04-25 18:17:37 UTC (rev 30168)
+++ ZODB/trunk/NEWS.txt	2005-04-25 18:26:11 UTC (rev 30169)
@@ -3,10 +3,29 @@
 Release date: DD-MMM-YYYY
 
 
+What's new in ZODB3 3.4a5?
+==========================
+Release date: 25-Apr-2005
+
+This was an internal release, to fix problems with the new savepoint feature.
+
+
 What's new in ZODB3 3.4a4?
 ==========================
 Release date: 23-Apr-2005
 
+This was an internal release, to create a tag for use in Zope 2.8b1 and
+Zope3 development.
+
+transaction
+-----------
+
+Transactions now support savepoints.  Savepoints allow changes to be
+periodically checkpointed within a transaction.  You can then
+rollback to a previously created savepoint.  See
+``transaction/savepoint.txt``.
+
+
 ZEO
 ---
 
@@ -67,7 +86,13 @@
 Since recursion isn't actually needed, and there was no other use for
 ``last=``, removing it everywhere was the obvious choice.
 
+Support for ZODB4 savepoint-aware data managers has been dropped
+----------------------------------------------------------------
 
+In adding savepoint support, we dropped the attempted support
+for ZODB4 data managers that support savepoints.  We don't think that
+this will affect anyone.
+
 What's new in ZODB3 3.4a3?
 ==========================
 Release date: 13-Apr-2005

Modified: ZODB/trunk/src/ZODB/Connection.py
===================================================================
--- ZODB/trunk/src/ZODB/Connection.py	2005-04-25 18:17:37 UTC (rev 30168)
+++ ZODB/trunk/src/ZODB/Connection.py	2005-04-25 18:26:11 UTC (rev 30169)
@@ -27,7 +27,7 @@
 # interfaces
 from persistent.interfaces import IPersistentDataManager
 from ZODB.interfaces import IConnection
-from transaction.interfaces import IDataManager
+from transaction.interfaces import ISavepointDataManager, IDataManagerSavepoint
 from zope.interface import implements
 
 import transaction
@@ -59,7 +59,7 @@
 class Connection(ExportImport, object):
     """Connection to ZODB for loading and storing objects."""
 
-    implements(IConnection, IDataManager, IPersistentDataManager)
+    implements(IConnection, ISavepointDataManager, IPersistentDataManager)
 
     _storage = _normal_storage = _savepoint_storage = None
 
@@ -319,7 +319,7 @@
     ##########################################################################
 
     ##########################################################################
-    # Data manager (IDataManager) methods
+    # Data manager (ISavepointDataManager) methods
 
     def abort(self, transaction):
         """Abort a transaction and forget all changes."""
@@ -638,7 +638,7 @@
         """Return a consistent sort key for this connection."""
         return "%s:%s" % (self._storage.sortKey(), id(self))
 
-    # Data manager (IDataManager) methods
+    # Data manager (ISavepointDataManager) methods
     ##########################################################################
 
     ##########################################################################
@@ -1061,6 +1061,8 @@
 
 class Savepoint:
 
+    implements(IDataManagerSavepoint)
+
     def __init__(self, datamanager, state):
         self.datamanager = datamanager
         self.state = state

Modified: ZODB/trunk/src/ZODB/tests/testConnectionSavepoint.py
===================================================================
--- ZODB/trunk/src/ZODB/tests/testConnectionSavepoint.py	2005-04-25 18:17:37 UTC (rev 30168)
+++ ZODB/trunk/src/ZODB/tests/testConnectionSavepoint.py	2005-04-25 18:26:11 UTC (rev 30169)
@@ -20,7 +20,20 @@
 import persistent.dict, transaction
 
 def testAddingThenModifyThenAbort():
-    """
+    """\
+
+We ran into a problem in which abort failed after adding an object in
+a savepoint and then modifying the object. The problem was that, on
+commit, the savepoint was aborted before the modifications were
+aborted.  Because the object was added in the savepoint, it's _p_oid
+and _p_jar were cleared when the savepoint was aborted.  The object
+was in the registered-object list.  There's an invariant for this
+lists that states that all objects in the list should have an oid and
+(correct) jar.
+
+The fix was to abort work done after he savepoint before aborting the
+savepoint.
+    
     >>> import ZODB.tests.util
     >>> db = ZODB.tests.util.DB()
     >>> connection = db.open()
@@ -35,7 +48,20 @@
 """
 
 def testModifyThenSavePointThenModifySomeMoreThenCommit():
-    """
+    """\
+
+We got conflict errors when we committed after we modified an object
+in a savepoint and then modified it some more after the last
+savepoint.
+
+The problem was that we were effectively commiting the object twice --
+when commiting the current data and when committing the savepoint.
+The fix was to first make a new savepoint to move new changes to the
+savepoint storage and *then* to commit the savepoint storage. (This is
+similar to thr strategy that was used for subtransactions prior to
+savepoints.)
+
+
     >>> import ZODB.tests.util
     >>> db = ZODB.tests.util.DB()
     >>> connection = db.open()

Modified: ZODB/trunk/src/transaction/_transaction.py
===================================================================
--- ZODB/trunk/src/transaction/_transaction.py	2005-04-25 18:17:37 UTC (rev 30168)
+++ ZODB/trunk/src/transaction/_transaction.py	2005-04-25 18:26:11 UTC (rev 30169)
@@ -231,17 +231,17 @@
 
     # Raise TransactionFailedError, due to commit()/join()/register()
     # getting called when the current transaction has already suffered
-    # a commit failure.
-    def _prior_commit_failed(self):
+    # a commit/savepoint failure.
+    def _prior_operation_failed(self):
         from ZODB.POSException import TransactionFailedError
         assert self._failure_traceback is not None
-        raise TransactionFailedError("commit() previously failed, "
-                "with this traceback:\n\n%s" %
+        raise TransactionFailedError("An operation previously failed, "
+                "with traceback:\n\n%s" %
                 self._failure_traceback.getvalue())
 
     def join(self, resource):
         if self.status is Status.COMMITFAILED:
-            self._prior_commit_failed() # doesn't return
+            self._prior_operation_failed() # doesn't return
 
         if self.status is not Status.ACTIVE:
             # TODO: Should it be possible to join a committing transaction?
@@ -261,15 +261,13 @@
 
     def savepoint(self, optimistic=False):
         if self.status is Status.COMMITFAILED:
-            self._prior_commit_failed() # doesn't return, it raises
+            self._prior_operation_failed() # doesn't return, it raises
 
         try:
-            savepoint = Savepoint(optimistic)
-            for resource in self._resources:
-                savepoint.join(resource)
+            savepoint = Savepoint(self, optimistic, *self._resources)
         except:
             self._cleanup(self._resources)
-            self._saveCommitishError() # doesn't return, it raises!
+            self._saveCommitishError() # reraises!
             
         if self._last_savepoint is not None:
             savepoint.previous = self._last_savepoint
@@ -330,7 +328,7 @@
             return
         
         if self.status is Status.COMMITFAILED:
-            self._prior_commit_failed() # doesn't return
+            self._prior_operation_failed() # doesn't return
 
         self._callBeforeCommitHooks()
 
@@ -598,30 +596,52 @@
     """
     interface.implements(interfaces.ISavepoint)
 
-    def __init__(self, optimistic):
-        self._savepoints = []
+    def __init__(self, transaction, optimistic, *resources):
+        self.transaction = transaction
+        self._savepoints = savepoints = []
         self.valid = True
         self.next = self.previous = None
         self.optimistic = optimistic
+
+        for datamanager in resources:
+            try:
+                savepoint = datamanager.savepoint
+            except AttributeError:
+                if not self.optimistic:
+                    raise TypeError("Savepoints unsupported", datamanager)
+                savepoint = NoRollbackSavepoint(datamanager)
+            else:
+                savepoint = savepoint()
+                
+            savepoints.append(savepoint)
     
     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)
+        
+        # A data manager has joined a transaction *after* a savepoint
+        # was created.  A couple of things are different in this case:
+        
+        # 1. We need to add it's savepoint to all previous savepoints.
+        # so that if they are rolled back, we roll this was back too.
+        
+        # 2. We don't actualy need to ask it for a savepoint.  Because
+        # is just joining, then we can abort it if there is an error,
+        # so we use an AbortSavepoint.
 
+        savepoint = AbortSavepoint(datamanager, self.transaction)
+        while self is not None:
+            self._savepoints.append(savepoint)
+            self = self.previous
+
     def rollback(self):
         if not self.valid:
             raise interfaces.InvalidSavepointRollbackError
         self._invalidate_next()
-        for savepoint in self._savepoints:
-            savepoint.rollback()
+        try:
+            for savepoint in self._savepoints:
+                savepoint.rollback()
+        except:
+            # Mark the transaction as failed
+            self.transaction._saveCommitishError() # reraises!
 
     def _invalidate_next(self):
         self.valid = False
@@ -633,6 +653,15 @@
         if self.previous is not None:
             self.previous._invalidate_previous()
 
+class AbortSavepoint:
+
+    def __init__(self, datamanager, transaction):
+        self.datamanager = datamanager
+        self.transaction = transaction
+
+    def rollback(self):
+        self.datamanager.abort(self.transaction)
+
 class NoRollbackSavepoint:
 
     def __init__(self, datamanager):

Modified: ZODB/trunk/src/transaction/interfaces.py
===================================================================
--- ZODB/trunk/src/transaction/interfaces.py	2005-04-25 18:17:37 UTC (rev 30168)
+++ ZODB/trunk/src/transaction/interfaces.py	2005-04-25 18:26:11 UTC (rev 30169)
@@ -19,12 +19,11 @@
 import zope.interface
 
 class ITransactionManager(zope.interface.Interface):
-    """An object that manages a sequence of transactions
+    """An object that manages a sequence of transactions.
 
     Applications use transaction managers to establish transaction boundaries.
     """
 
-
     def begin():
         """Begin a new transaction.
 
@@ -36,19 +35,28 @@
         """
 
     def commit():
-        """Commit the current transaction
+        """Commit the current transaction.
         """
 
-    def abort(self):
-        """Abort the current transaction
+    def abort():
+        """Abort the current transaction.
         """
 
+    def savepoint(optimistic=False):
+        """Create a savepoint from the current transaction.
+
+        If the optimistic argument is true, then data managers that
+        don't support savepoints can be used, but an error will be
+        raised if the savepoint is rolled back.
+
+        An ISavepoint object is returned.
+        """
+
     def registerSynch(synch):
         """Register an ISynchronizer.
 
         Synchronizers are notified at the beginning and end of
         transaction completion.
-        
         """
 
     def unregisterSynch(synch):
@@ -56,7 +64,6 @@
 
         Synchronizers are notified at the beginning and end of
         transaction completion.
-        
         """
 
 class ITransaction(zope.interface.Interface):
@@ -91,34 +98,43 @@
         raise an exception, or truncate the value).
         """)
 
-    def commit(subtransaction=None):
+    def commit():
         """Finalize the transaction.
 
         This executes the two-phase commit algorithm for all
         IDataManager objects associated with the transaction.
         """
 
-    def abort(subtransaction=0, freeme=1):
+    def abort():
         """Abort the transaction.
 
         This is called from the application.  This can only be called
         before the two-phase commit protocol has been started.
         """
 
+    def savepoint(optimistic=False):
+        """Create a savepoint.
+
+        If the optimistic argument is true, then data managers that don't
+        support savepoints can be used, but an error will be raised if the
+        savepoint is rolled back.
+
+        An ISavepoint object is returned.
+        """
+
     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.
+        If the data manager supports savepoints, it must call join *before*
+        making any changes:  if the transaction has made any savepoints, then
+        the transaction will take a savepoint of the data manager when join
+        is called, and this savepoint must reflect 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):
@@ -195,16 +211,12 @@
     """Objects that manage transactional storage.
 
     These objects may manage data for other objects, or they may manage
-    non-object storages, such as relational databases.
+    non-object storages, such as relational databases.  For example,
+    a ZODB.Connection.
 
-    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
+    Note that when some data is modified, that data's data manager 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
@@ -225,17 +237,6 @@
 
         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):
@@ -263,7 +264,7 @@
         """
 
     def tpc_vote(transaction):
-        """Verify that a data manager can commit the 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.
@@ -290,7 +291,7 @@
         """
 
     def sortKey():
-        """Return a key to use for ordering registered DataManagers
+        """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
@@ -308,32 +309,47 @@
 class ISavepointDataManager(IDataManager):
 
     def savepoint():
-        """Return a savepoint (ISavepoint)
+        """Return a data-manager savepoint (IDataManagerSavepoint).
         """
 
+class IDataManagerSavepoint(zope.interface.Interface):
+    """Savepoint for data-manager changes for use in transaction savepoints.
+
+    Datamanager savepoints are used by, and only by, transaction savepoints.
+
+    Note that data manager savepoints don't have any notion of, or
+    responsibility for, validity.  It isn't the responsibility of
+    data-manager savepoints to prevent multiple rollbacks or rollbacks after
+    transaction termination.  Preventing invalid savepoint rollback is the
+    responsibility of transaction rollbacks. Application code should never
+    use data-manager savepoints.
+    """
+
+    def rollback():
+        """Rollback any work done since the savepoint.
+        """
+
 class ISavepoint(zope.interface.Interface):
-    """A transaction savepoint
+    """A transaction savepoint.
     """
 
     def rollback():
-        """Rollback any work done since the savepoint
+        """Rollback any work done since the savepoint.
 
-        An InvalidSavepointRollbackError is raised if the savepoint
-        isn't valid.
-        
+        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
+    """Attempt to rollback an invalid savepoint.
 
     A savepoint may be invalid because:
 
-    - The surrounding transaction has committed or aborted
+    - The surrounding transaction has committed or aborted.
 
-    - An earlier savepoint in the same transaction has been rolled back
+    - An earlier savepoint in the same transaction has been rolled back.
     """
 
 class ISynchronizer(zope.interface.Interface):
@@ -347,4 +363,3 @@
     def afterCompletion(transaction):
         """Hook that is called by the transaction after completing a commit.
         """
-

Modified: ZODB/trunk/src/transaction/savepoint.txt
===================================================================
--- ZODB/trunk/src/transaction/savepoint.txt	2005-04-25 18:17:37 UTC (rev 30168)
+++ ZODB/trunk/src/transaction/savepoint.txt	2005-04-25 18:26:11 UTC (rev 30169)
@@ -201,7 +201,7 @@
     >>> transaction.abort()
     
 However, a flag can be passed to the transaction savepoint method to
-indicate that databases without savepoint support should be tolderated
+indicate that databases without savepoint support should be tolerated
 until a savepoint is roled back.  This allows transactions to proceed
 is there are no reasons to roll back:
 
@@ -212,10 +212,64 @@
     >>> dm_no_sp['name']
     'sue'
 
+    >>> dm_no_sp['name'] = 'sam'
     >>> savepoint = transaction.savepoint(1)
-    >>> dm_no_sp['name'] = 'sam'
     >>> savepoint.rollback()
     Traceback (most recent call last):
     ...
     TypeError: ('Savepoints unsupported', {'name': 'sam'})
 
+Failures
+--------
+
+If a failure occurs when creating or rolling back a savepoint, the
+transaction state will be uncertain and the transaction will become
+uncommitable.  From that point on, most transaction operations,
+including commit, will fail until the transaction is aborted.
+
+In the previous example, we got an error when we tried to rollback the
+savepoint. If we try to commit the transaction, the commit will fail:
+
+    >>> transaction.commit() # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    TransactionFailedError: An operation previously failed, with traceback:
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sam'})
+    <BLANKLINE>
+
+We have to abort it to make any progress:
+
+    >>> transaction.abort()
+
+Similarly, in our earlier example, where we tried to take a savepoint
+with a data manager that didn't support savepoints:
+
+    >>> dm_no_sp['name'] = 'sally'
+    >>> dm['name'] = 'sally'
+    >>> savepoint = transaction.savepoint()
+    Traceback (most recent call last):
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sue'})
+
+    >>> transaction.commit() # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    TransactionFailedError: An operation previously failed, with traceback:
+    ...
+    TypeError: ('Savepoints unsupported', {'name': 'sue'})
+    <BLANKLINE>
+    
+    >>> transaction.abort()
+ 
+After clearing the transaction with an abort, we can get on with new
+transactions: 
+
+    >>> dm_no_sp['name'] = 'sally'
+    >>> dm['name'] = 'sally'
+    >>> transaction.commit()
+    >>> dm_no_sp['name']
+    'sally'
+    >>> dm['name']
+    'sally'
+    

Modified: ZODB/trunk/src/transaction/tests/savepointsample.py
===================================================================
--- ZODB/trunk/src/transaction/tests/savepointsample.py	2005-04-25 18:17:37 UTC (rev 30168)
+++ ZODB/trunk/src/transaction/tests/savepointsample.py	2005-04-25 18:26:11 UTC (rev 30169)
@@ -30,7 +30,7 @@
     This data manager stores named simple values, like strings and numbers.
     """
     
-    interface.implements(transaction.interfaces.ISavepointDataManager)
+    interface.implements(transaction.interfaces.IDataManager)
 
     def __init__(self, transaction_manager = None):
         if transaction_manager is None:
@@ -156,6 +156,8 @@
     This extends the basic data manager with savepoint support.
     """
 
+    interface.implements(transaction.interfaces.ISavepointDataManager)
+
     def savepoint(self):
         # When we create the savepoint, we save the existing database state
         return SampleSavepoint(self, self.uncommitted.copy())
@@ -166,6 +168,8 @@
 
 class SampleSavepoint:
 
+    interface.implements(transaction.interfaces.IDataManagerSavepoint)
+
     def __init__(self, data_manager, data):
         self.data_manager = data_manager
         self.data = data

Modified: ZODB/trunk/src/transaction/tests/test_savepoint.py
===================================================================
--- ZODB/trunk/src/transaction/tests/test_savepoint.py	2005-04-25 18:17:37 UTC (rev 30168)
+++ ZODB/trunk/src/transaction/tests/test_savepoint.py	2005-04-25 18:26:11 UTC (rev 30169)
@@ -19,9 +19,49 @@
 from zope.testing import doctest
 
 
+def testRollbackRollsbackDataManagersThatJoinedLater():
+    """
+
+A savepoint needs to not just rollback it's savepoints, but needs to
+rollback savepoints for data managers that joined savepoints after the
+savepoint:
+
+    >>> import transaction.tests.savepointsample
+    >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
+    >>> dm['name'] = 'bob'
+    >>> sp1 = transaction.savepoint()
+    >>> dm['job'] = 'geek'
+    >>> sp2 = transaction.savepoint()
+    >>> dm['salary'] = 'fun'    
+    >>> dm2 = transaction.tests.savepointsample.SampleSavepointDataManager()
+    >>> dm2['name'] = 'sally'
+
+    >>> 'name' in dm
+    True
+    >>> 'job' in dm
+    True
+    >>> 'salary' in dm
+    True
+    >>> 'name' in dm2
+    True
+
+    >>> sp1.rollback()
+
+    >>> 'name' in dm
+    True
+    >>> 'job' in dm
+    False
+    >>> 'salary' in dm
+    False
+    >>> 'name' in dm2
+    False
+
+"""
+
 def test_suite():
     return unittest.TestSuite((
         doctest.DocFileSuite('../savepoint.txt'),
+        doctest.DocTestSuite(),
         ))
 
 if __name__ == '__main__':



More information about the Zodb-checkins mailing list