[Zope3-checkins] CVS: Zope3/src/transaction/tests - test_SampleDataManager.py:1.1.2.2

Jim Fulton jim at zope.com
Thu Jan 22 14:44:26 EST 2004


Update of /cvs-repository/Zope3/src/transaction/tests
In directory cvs.zope.org:/tmp/cvs-serv30783

Modified Files:
      Tag: zope3-zodb3-devel-branch
	test_SampleDataManager.py 
Log Message:
This test module exists for two reasons:

- To provide details of the ZODB 4 transaction protocol, as I
  understand them.

- To provide a test stub that can be used to test the zodb3
  data-manager adapter.


=== Zope3/src/transaction/tests/test_SampleDataManager.py 1.1.2.1 => 1.1.2.2 ===
--- Zope3/src/transaction/tests/test_SampleDataManager.py:1.1.2.1	Thu Jan 22 11:32:20 2004
+++ Zope3/src/transaction/tests/test_SampleDataManager.py	Thu Jan 22 14:44:26 2004
@@ -19,72 +19,72 @@
 class DataManager(object):
     """Sample data manager
 
-    This class provides a trivial data-manager implementation and doc
-    strings to illustrate the the protocol and to provide a tool for
-    writing tests.
+       This class provides a trivial data-manager implementation and doc
+       strings to illustrate the the protocol and to provide a tool for
+       writing tests.
 
-    Our sample data manager has state that is updated through an inc
-    method and through transaction operations.
+       Our sample data manager has state that is updated through an inc
+       method and through transaction operations.
 
-    When we create a sample data manager:
+       When we create a sample data manager:
 
-    >>> dm = DataManager()
+       >>> dm = DataManager()
 
-    It has two bits of state, state:
+       It has two bits of state, state:
 
-    >>> dm.state
-    0
+       >>> dm.state
+       0
 
-    and delta:
+       and delta:
 
-    >>> dm.delta
-    0
+       >>> dm.delta
+       0
 
-    Both of which are initialized to 0.  state is meant to model
-    committed state, while delta represents tentative changes within a
-    transaction.  We change the state by calling inc:
+       Both of which are initialized to 0.  state is meant to model
+       committed state, while delta represents tentative changes within a
+       transaction.  We change the state by calling inc:
 
-    >>> dm.inc()
+       >>> dm.inc()
 
-    which updates delta:
+       which updates delta:
 
-    >>> dm.delta
-    1
+       >>> dm.delta
+       1
 
-    but state isn't changed until we commit the transaction:
+       but state isn't changed until we commit the transaction:
 
-    >>> dm.state
-    0
+       >>> dm.state
+       0
 
-    To commit the changes, we use 2-phase commit. We execute the first
-    stage by calling prepare.  We need to pass a transation. Our
-    sample data managers don't really use the transactions for much,
-    so we'll be lazy and use integers for transactions:
+       To commit the changes, we use 2-phase commit. We execute the first
+       stage by calling prepare.  We need to pass a transation. Our
+       sample data managers don't really use the transactions for much,
+       so we'll be lazy and use strings for transactions:
 
-    >>> t1 = 1
-    >>> dm.prepare(t1)
+       >>> t1 = '1'
+       >>> dm.prepare(t1)
 
-    The sample data manager updates the state when we call prepare:
+       The sample data manager updates the state when we call prepare:
 
-    >>> dm.state
-    1
-    >>> dm.delta
-    1
+       >>> dm.state
+       1
+       >>> dm.delta
+       1
 
-    This is mainly so we can detect some affect of calling the methods.
+       This is mainly so we can detect some affect of calling the methods.
 
-    Now if we call commit:
+       Now if we call commit:
 
-    >>> dm.commit(t1)
+       >>> dm.commit(t1)
 
-    Our changes are"permanent".  The state reflects the changes and the
-    delta has been reset to 0. 
+       Our changes are"permanent".  The state reflects the changes and the
+       delta has been reset to 0. 
 
-    >>> dm.state
-    1
-    >>> dm.delta
-    0
-    """
+       >>> dm.state
+       1
+       >>> dm.delta
+       0
+       """
 
     def __init__(self):
         self.state = 0
@@ -101,35 +101,44 @@
 
         >>> dm = DataManager()
         >>> dm.inc()
-        >>> dm.prepare(1)
-        >>> dm.commit(1)
+        >>> t1 = '1'
+        >>> dm.prepare(t1)
+        >>> dm.commit(t1)
         >>> dm.state
         1
         >>> dm.inc()
-        >>> dm.prepare(2)
-        >>> dm.abort(2)
+        >>> t2 = '2'
+        >>> dm.prepare(t2)
+        >>> dm.abort(t2)
         >>> dm.state
         1
 
         It is en error to call prepare more than once without an intervening
         commit or abort:
 
-        >>> dm.prepare(1)
-Traceback (most recent call last):
+        >>> dm.prepare(t1)
 
-TypeError: Already prepared
+        >>> dm.prepare(t1)
+        Traceback (most recent call last):
+        ...
+        TypeError: Already prepared
+
+        >>> dm.prepare(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: Already prepared
 
-        >>> dm.prepare(1)
-        >>> dm.prepare(2)
-
-        >>> dm.abort(1)
+        >>> dm.abort(t1)
 
         If there was a preceeding savepoint, the transaction must match:
 
-        >>> rollback = dm.savepoint(1)
-        >>> rollback.prepare(2)
+        >>> rollback = dm.savepoint(t1)
+        >>> dm.prepare(t2)
+        Traceback (most recent call last):
+        ,,,
+        TypeError: ('Transaction missmatch', '2', '1')
 
-        >>> rollback.prepare(1)
+        >>> dm.prepare(t1)
         
         """
         if self.prepared:
@@ -148,15 +157,121 @@
     def abort(self, transaction):
         """Abort a transaction
 
+        The abort method can be called before two-phase commit to
+        throw away work done in the transaction:
+
+        >>> dm = DataManager()
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (0, 1)
+        >>> t1 = '1'
+        >>> dm.abort(t1)
+        >>> dm.state, dm.delta
+        (0, 0)
+
+        The abort method also throws away work done in savepoints:
+
+        >>> dm.inc()
+        >>> r = dm.savepoint(t1)
+        >>> dm.inc()
+        >>> r = dm.savepoint(t1)
+        >>> dm.state, dm.delta
+        (0, 2)
+        >>> dm.abort(t1)
+        >>> dm.state, dm.delta
+        (0, 0)
+
+        If savepoints are used, abort must be passed the same
+        transaction:
+        
+        >>> dm.inc()
+        >>> r = dm.savepoint(t1)
+        >>> t2 = '2'
+        >>> dm.abort(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Transaction missmatch', '2', '1')
+
+        >>> dm.abort(t1)
+
+        The abort method is also used to abort a two-phase commit:
+
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (0, 1)
+        >>> dm.prepare(t1)
+        >>> dm.state, dm.delta
+        (1, 1)
+        >>> dm.abort(t1)
+        >>> dm.state, dm.delta
+        (0, 0)
+
+        Of course, the transactions passed to prepare and abort must
+        match:
+        
+        >>> dm.prepare(t1)
+        >>> dm.abort(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Transaction missmatch', '2', '1')
+        
+        >>> dm.abort(t1)
+        
+
         """
         self._checkTransaction(transaction)
         if self.transaction is not None:
-            self.state -= self.delta
             self.transaction = None
+
+        if self.prepared:
+            self.state -= self.delta
+            self.prepared = False
+
         self.delta = 0
-        self.prepared = False
 
     def commit(self, transaction):
+        """Complete two-phase commit
+
+        >>> dm = DataManager()
+        >>> dm.state
+        0
+        >>> dm.inc()
+
+        We start two-phase commit by calling prepare:
+
+        >>> t1 = '1'
+        >>> dm.prepare(t1)
+
+        We complete it by calling commit:
+
+        >>> dm.commit(t1)
+        >>> dm.state
+        1
+
+        It is an error ro call commit without calling prepare first:
+
+        >>> dm.inc()
+        >>> t2 = '2'
+        >>> dm.commit(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: Not prepared to commit
+
+        >>> dm.prepare(t2)
+        >>> dm.commit(t2)
+
+        If course, the transactions given to prepare and commit must
+        be the same:
+        
+        >>> dm.inc()
+        >>> t3 = '3'
+        >>> dm.prepare(t3)
+        >>> dm.commit(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Transaction missmatch', '2', '3')
+        
+        """
         if not self.prepared:
             raise TypeError('Not prepared to commit')
         self._checkTransaction(transaction)
@@ -165,6 +280,105 @@
         self.prepared = False
 
     def savepoint(self, transaction):
+        """Provide the ability to rollback transaction state
+
+        Savepoints provide a way to:
+
+        - Save partial transaction work. For some data managers, this
+          could allow resources to be used more efficiently.
+
+        - Provide the ability to revert state to a point in a
+          transaction without aborting the entire transaction.  In
+          other words, savepoints support partial aborts.
+
+        Savepoints don't use two-phase commit. If there are errors in
+        setting or rolling back to savepoints, the application should
+        abort the containing transaction.  This is *not* the
+        responsibility of the data manager.
+
+        Savepoints are always associated with a transaction. Any work
+        done in a savepoint's transaction is tentative until the
+        transaction is committed using two-phase commit.
+
+        >>> dm = DataManager()
+        >>> dm.inc()
+        >>> t1 = '1'
+        >>> r = dm.savepoint(t1)
+        >>> dm.state, dm.delta
+        (0, 1)
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (0, 2)
+        >>> r.rollback()
+        >>> dm.state, dm.delta
+        (0, 1)
+        >>> dm.prepare(t1)
+        >>> dm.commit(t1)
+        >>> dm.state, dm.delta
+        (1, 0)
+
+        Savepoints must have the same transaction:
+
+        >>> r1 = dm.savepoint(t1)
+        >>> dm.state, dm.delta
+        (1, 0)
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (1, 1)
+        >>> t2 = '2'
+        >>> r2 = dm.savepoint(t2)
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Transaction missmatch', '2', '1')
+
+        >>> r2 = dm.savepoint(t1)
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (1, 2)
+
+        If we rollback to an earlier savepoint, we discard all work
+        done later:
+
+        >>> r1.rollback()
+        >>> dm.state, dm.delta
+        (1, 0)
+
+        and we can no longer rollback to the later savepoint:
+
+        >>> r2.rollback()
+        Traceback (most recent call last):
+        ...
+        TypeError: ('Attempt to roll back to invalid save point', 3, 2)
+
+        We can roll back to a savepoint as often as we like:
+
+        >>> r1.rollback()
+        >>> r1.rollback()
+        >>> r1.rollback()
+        >>> dm.state, dm.delta
+        (1, 0)
+
+        >>> dm.inc()
+        >>> dm.inc()
+        >>> dm.inc()
+        >>> dm.state, dm.delta
+        (1, 3)
+        >>> r1.rollback()
+        >>> dm.state, dm.delta
+        (1, 0)
+
+        But we can't rollback to a savepoint after it has been
+        committed:
+
+        >>> dm.prepare(t1)
+        >>> dm.commit(t1)
+
+        >>> r1.rollback()
+        Traceback (most recent call last):
+        ...
+        TypeError: Attempt to rollback stale rollback
+
+        """
         if self.prepared:
             raise TypeError("Can't get savepoint during two-phase commit")
         self._checkTransaction(transaction)
@@ -177,18 +391,17 @@
     def __init__(self, dm):
         self.dm = dm
         self.sp = dm.sp
-        self.state = dm.state
+        self.delta = dm.delta
         self.transaction = dm.transaction
 
     def rollback(self):
-        if self.transaction is not self.sp.transaction:
+        if self.transaction is not self.dm.transaction:
             raise TypeError("Attempt to rollback stale rollback")
         if self.dm.sp < self.sp:
             raise TypeError("Attempt to roll back to invalid save point",
                             self.sp, self.dm.sp)
         self.dm.sp = self.sp
-        self.dm.state = self.state
-        self.dm.delta = 0
+        self.dm.delta = self.delta
 
 
 def test_suite():




More information about the Zope3-Checkins mailing list