[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