[Zodb-checkins] SVN: ZODB/branches/3.4/ ISynchronizer grows a
newTransaction() method, called
Tim Peters
tim.one at comcast.net
Thu May 5 21:17:46 EDT 2005
Log message for revision 30255:
ISynchronizer grows a newTransaction() method, called
whenever TransactionManager.begin() is called.
Connection implements that, and changes its ISynchronizer
afterCompletion() method, to call sync() on its storage
(if the storage has such a method), and to process
invalidations in any case.
The bottom line is that storage sync() will get done "by
magic" now after top-level commit() and abort(), and after
explicit TransactionManager.begin(). This should make it
possible to deprecate Connection.sync(), although I'm not
doing that yet. Made a small but meaningful start by
purging many sync() calls from some of the nastiest ZEO
tests -- and they still work fine.
Changed:
U ZODB/branches/3.4/NEWS.txt
U ZODB/branches/3.4/src/ZEO/tests/ConnectionTests.py
U ZODB/branches/3.4/src/ZEO/tests/InvalidationTests.py
U ZODB/branches/3.4/src/ZODB/Connection.py
U ZODB/branches/3.4/src/ZODB/interfaces.py
A ZODB/branches/3.4/src/ZODB/tests/synchronizers.txt
U ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py
U ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.txt
U ZODB/branches/3.4/src/ZODB/tests/test_doctest_files.py
U ZODB/branches/3.4/src/transaction/_manager.py
U ZODB/branches/3.4/src/transaction/interfaces.py
-=-
Modified: ZODB/branches/3.4/NEWS.txt
===================================================================
--- ZODB/branches/3.4/NEWS.txt 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/NEWS.txt 2005-05-06 01:17:46 UTC (rev 30255)
@@ -5,9 +5,6 @@
transaction
-----------
-- A ``getBeforeCommitHooks()`` method was added. It returns an iterable
- producing the registered beforeCommit hooks.
-
- Doing a subtransaction commit erroneously processed invalidations, which
could lead to an inconsistent view of the database. For example, let T be
the transaction of which the subtransaction commit was a part. If T read a
@@ -24,7 +21,25 @@
could fail, and despite that T never modifed O.
+- A ``getBeforeCommitHooks()`` method was added. It returns an iterable
+ producing the registered beforeCommit hooks.
+- The ``ISynchronizer`` interface has a new ``newTransaction()`` method.
+ This is invoked whenever a transaction manager's ``begin()`` method is
+ called. (Note that a transaction object's (as opposed to a transaction
+ manager's) ``begin()`` method is deprecated, and ``newTransaction()``
+ is not called when using the deprecated method.)
+
+- Relatedly, ``Connection`` implements ``ISynchronizer``, and ``Connection``'s
+ ``afterCompletion()`` and ``newTransaction()`` methods now call ``sync()``
+ on the underlying storage (if the underlying storage has such a method),
+ in addition to processing invalidations. The practical implication is that
+ storage synchronization will be done automatically now, whenever a
+ transaction is explicitly started, and after top-level transaction commit
+ or abort. As a result, ``Connection.sync()`` should virtually never be
+ needed anymore, and will eventually be deprecated.
+
+
What's new in ZODB3 3.4a5?
==========================
Release date: 25-Apr-2005
Modified: ZODB/branches/3.4/src/ZEO/tests/ConnectionTests.py
===================================================================
--- ZODB/branches/3.4/src/ZEO/tests/ConnectionTests.py 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/ZEO/tests/ConnectionTests.py 2005-05-06 01:17:46 UTC (rev 30255)
@@ -640,6 +640,7 @@
r1["a"] = MinPO("a")
transaction.commit()
+ self.assertEqual(r1._p_state, 0) # up-to-date
db2 = DB(self.openClientStorage())
r2 = db2.open().root()
@@ -649,18 +650,16 @@
r2["b"] = MinPO("b")
transaction.commit()
- # make sure the invalidation is received in the other client
+ # Make sure the invalidation is received in the other client.
for i in range(10):
- c1._storage.sync()
- if c1._invalidated.has_key(r1._p_oid):
+ if r1._p_state == -1:
break
time.sleep(0.1)
- self.assert_(c1._invalidated.has_key(r1._p_oid))
+ self.assertEqual(r1._p_state, -1) # ghost
- # force the invalidations to be applied...
- c1.sync()
r1.keys() # unghostify
self.assertEqual(r1._p_serial, r2._p_serial)
+ self.assertEqual(r1["b"].value, "b")
db2.close()
db1.close()
Modified: ZODB/branches/3.4/src/ZEO/tests/InvalidationTests.py
===================================================================
--- ZODB/branches/3.4/src/ZEO/tests/InvalidationTests.py 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/ZEO/tests/InvalidationTests.py 2005-05-06 01:17:46 UTC (rev 30255)
@@ -88,8 +88,7 @@
try:
self.tm.get().commit()
except ConflictError, msg:
- self.tm.get().abort()
- cn.sync()
+ self.tm.abort()
else:
if self.sleep:
time.sleep(self.sleep)
@@ -152,7 +151,6 @@
break
except (ConflictError, KeyError):
transaction.abort()
- cn.sync()
key = self.startnum
while not self.stop.isSet():
try:
@@ -164,11 +162,6 @@
time.sleep(self.sleep)
except (ReadConflictError, ConflictError), msg:
transaction.abort()
- # sync() is necessary here to process invalidations
- # if we get a read conflict. In the read conflict case,
- # no objects were modified so cn never got registered
- # with the transaction.
- cn.sync()
else:
self.added_keys.append(key)
key += self.step
@@ -201,7 +194,6 @@
except (ConflictError, KeyError):
# print "%d getting tree abort" % self.threadnum
transaction.abort()
- cn.sync()
keys_added = {} # set of keys we commit
tkeys = []
@@ -223,7 +215,6 @@
except (ReadConflictError, ConflictError), msg:
# print "%d setting key %s" % (self.threadnum, msg)
transaction.abort()
- cn.sync()
break
else:
# print "%d set #%d" % (self.threadnum, len(keys))
@@ -236,16 +227,10 @@
except ConflictError, msg:
# print "%d commit %s" % (self.threadnum, msg)
transaction.abort()
- cn.sync()
continue
for k in keys:
tkeys.remove(k)
keys_added[k] = 1
- # sync() is necessary here to process invalidations
- # if we get a read conflict. In the read conflict case,
- # no objects were modified so cn never got registered
- # with the transaction.
- cn.sync()
self.added_keys = keys_added.keys()
cn.close()
@@ -287,7 +272,6 @@
break
except (ConflictError, KeyError):
transaction.abort()
- cn.sync()
while not self.stop.isSet():
try:
tree[key] = self.threadnum
@@ -297,11 +281,6 @@
break
except (VersionLockError, ReadConflictError, ConflictError), msg:
transaction.abort()
- # sync() is necessary here to process invalidations
- # if we get a read conflict. In the read conflict case,
- # no objects were modified so cn never got registered
- # with the transaction.
- cn.sync()
if self.sleep:
time.sleep(self.sleep)
try:
@@ -319,7 +298,6 @@
return commit
except ConflictError, msg:
transaction.abort()
- cn.sync()
finally:
cn.close()
return 0
@@ -351,7 +329,6 @@
except ReadConflictError:
if retries:
transaction.abort()
- cn.sync()
else:
raise
except:
Modified: ZODB/branches/3.4/src/ZODB/Connection.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/Connection.py 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/ZODB/Connection.py 2005-05-06 01:17:46 UTC (rev 30255)
@@ -27,7 +27,9 @@
# interfaces
from persistent.interfaces import IPersistentDataManager
from ZODB.interfaces import IConnection
-from transaction.interfaces import ISavepointDataManager, IDataManagerSavepoint
+from transaction.interfaces import ISavepointDataManager
+from transaction.interfaces import IDataManagerSavepoint
+from transaction.interfaces import ISynchronizer
from zope.interface import implements
import transaction
@@ -59,7 +61,10 @@
class Connection(ExportImport, object):
"""Connection to ZODB for loading and storing objects."""
- implements(IConnection, ISavepointDataManager, IPersistentDataManager)
+ implements(IConnection,
+ ISavepointDataManager,
+ IPersistentDataManager,
+ ISynchronizer)
_storage = _normal_storage = _savepoint_storage = None
@@ -291,11 +296,8 @@
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()
+ self._txn_mgr.abort()
+ self._storage_sync()
def getDebugInfo(self):
"""Returns a tuple with different items for debugging the
@@ -379,6 +381,7 @@
self._needs_to_join = True
self._registered_objects = []
+ # Process pending invalidations.
def _flush_invalidations(self):
self._inv_lock.acquire()
try:
@@ -650,10 +653,19 @@
# We don't do anything before a commit starts.
pass
- def afterCompletion(self, txn):
+ # Call the underlying storage's sync() method (if any), and process
+ # pending invalidations regardless. Of course this should only be
+ # called at transaction boundaries.
+ def _storage_sync(self, *ignored):
+ sync = getattr(self._storage, 'sync', 0)
+ if sync:
+ sync()
self._flush_invalidations()
- # Transaction-manager synchronization -- ISynchronizer
+ afterCompletion = _storage_sync
+ newTransaction = _storage_sync
+
+ # Transaction-manager synchronization -- ISynchronizer
##########################################################################
##########################################################################
Modified: ZODB/branches/3.4/src/ZODB/interfaces.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/interfaces.py 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/ZODB/interfaces.py 2005-05-06 01:17:46 UTC (rev 30255)
@@ -279,8 +279,8 @@
"""Manually update the view on the database.
This includes aborting the current transaction, getting a fresh and
- consistent view of the data (synchronizing with the storage if possible)
- and call cacheGC() for this connection.
+ consistent view of the data (synchronizing with the storage if
+ possible) and calling cacheGC() for this connection.
This method was especially useful in ZODB 3.2 to better support
read-only connections that were affected by a couple of problems.
Added: ZODB/branches/3.4/src/ZODB/tests/synchronizers.txt
===================================================================
--- ZODB/branches/3.4/src/ZODB/tests/synchronizers.txt 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/ZODB/tests/synchronizers.txt 2005-05-06 01:17:46 UTC (rev 30255)
@@ -0,0 +1,61 @@
+Here are some tests that storage sync() methods get called at appropriate
+times in the life of a transaction. The tested behavior is new in ZODB 3.4.
+
+First define a lightweight storage with a sync() method:
+
+ >>> import ZODB
+ >>> from ZODB.MappingStorage import MappingStorage
+ >>> import transaction
+
+ >>> class SimpleStorage(MappingStorage):
+ ... sync_called = False
+ ...
+ ... def sync(self, *args):
+ ... self.sync_called = True
+
+Make a change locally:
+
+ >>> st = SimpleStorage()
+ >>> db = ZODB.DB(st)
+ >>> cn = db.open()
+ >>> rt = cn.root()
+ >>> rt['a'] = 1
+
+Sync should not have been called yet.
+
+ >>> st.sync_called # False before 3.4
+ False
+
+
+sync is called by the Connection's afterCompletion() hook after the commit
+completes.
+
+ >>> transaction.commit()
+ >>> st.sync_called # False before 3.4
+ True
+
+sync is also called by the afterCompletion() hook after an abort.
+
+ >>> st.sync_called = False
+ >>> rt['b'] = 2
+ >>> transaction.abort()
+ >>> st.sync_called # False before 3.4
+ True
+
+And sync is called whenever we explicitly start a new txn, via the
+newTransaction() hook.
+
+ >>> st.sync_called = False
+ >>> dummy = transaction.begin()
+ >>> st.sync_called # False before 3.4
+ True
+
+
+Clean up. Closing db isn't enough -- closing a DB doesn't close its
+Connections. Leaving our Connection open here can cause the
+SimpleStorage.sync() method to get called later, during another test, and
+our doctest-synthesized module globals no longer exist then. You get
+a weird traceback then ;-)
+
+ >>> cn.close()
+ >>> db.close()
Property changes on: ZODB/branches/3.4/src/ZODB/tests/synchronizers.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Modified: ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.py 2005-05-06 01:17:46 UTC (rev 30255)
@@ -21,19 +21,18 @@
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
+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
+aborted. Because the object was added in the savepoint, its _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
+list 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
+The fix was to abort work done after the savepoint before aborting the
savepoint.
-
+
>>> import ZODB.tests.util
>>> db = ZODB.tests.util.DB()
>>> connection = db.open()
@@ -44,21 +43,19 @@
>>> sp = transaction.savepoint()
>>> ob.x = 1
>>> transaction.abort()
-
"""
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
+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
+similar to the strategy that was used for subtransactions prior to
savepoints.)
@@ -71,7 +68,6 @@
>>> sp = transaction.savepoint()
>>> root['a'] = 2
>>> transaction.commit()
-
"""
def test_suite():
@@ -82,4 +78,3 @@
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
-
Modified: ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.txt
===================================================================
--- ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.txt 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/ZODB/tests/testConnectionSavepoint.txt 2005-05-06 01:17:46 UTC (rev 30255)
@@ -44,7 +44,7 @@
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
+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
@@ -60,7 +60,7 @@
... 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
+unexpected way, it rolls back all of it's changes and
prints the error:
>>> def apply_entries(entries):
@@ -102,7 +102,7 @@
>>> root['sally-balance']
-80.0
-
+
If we give provide entries that cause an unexpected error:
>>> apply_entries([
@@ -115,7 +115,7 @@
Updated sally
Unexpected exception unsupported operand type(s) for +=: 'float' and 'str'
-Because the apply_entries used a savepoint for the entire function,
+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:
Modified: ZODB/branches/3.4/src/ZODB/tests/test_doctest_files.py
===================================================================
--- ZODB/branches/3.4/src/ZODB/tests/test_doctest_files.py 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/ZODB/tests/test_doctest_files.py 2005-05-06 01:17:46 UTC (rev 30255)
@@ -17,4 +17,5 @@
def test_suite():
return DocFileSuite("dbopen.txt",
"multidb.txt",
+ "synchronizers.txt",
)
Modified: ZODB/branches/3.4/src/transaction/_manager.py
===================================================================
--- ZODB/branches/3.4/src/transaction/_manager.py 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/transaction/_manager.py 2005-05-06 01:17:46 UTC (rev 30255)
@@ -32,6 +32,16 @@
# Obscure: because of the __init__.py maze, we can't import WeakSet
# at top level here.
+# Call the ISynchronizer newTransaction() method on every element of
+# WeakSet synchs.
+# A transaction manager needs to do this whenever begin() is called.
+# Since it would be good if tm.get() returned the new transaction while
+# newTransaction() is running, calling this has to be delayed until after
+# the transaction manager has done whatever it needs to do to make its
+# get() return the new txn.
+def _new_transaction(txn, synchs):
+ synchs.map(lambda s: s.newTransaction(txn))
+
class TransactionManager(object):
def __init__(self):
@@ -43,8 +53,9 @@
def begin(self):
if self._txn is not None:
self._txn.abort()
- self._txn = Transaction(self._synchs, self)
- return self._txn
+ txn = self._txn = Transaction(self._synchs, self)
+ _new_transaction(txn, self._synchs)
+ return txn
def get(self):
if self._txn is None:
@@ -91,6 +102,7 @@
txn.abort()
synchs = self._synchs.get(tid)
txn = self._txns[tid] = Transaction(synchs, self)
+ _new_transaction(txn, synchs)
return txn
def get(self):
Modified: ZODB/branches/3.4/src/transaction/interfaces.py
===================================================================
--- ZODB/branches/3.4/src/transaction/interfaces.py 2005-05-05 19:49:32 UTC (rev 30254)
+++ ZODB/branches/3.4/src/transaction/interfaces.py 2005-05-06 01:17:46 UTC (rev 30255)
@@ -28,6 +28,9 @@
"""Begin a new transaction.
If an existing transaction is in progress, it will be aborted.
+
+ The newTransaction() method of registered synchronizers is called,
+ passing the new transaction object.
"""
def get():
@@ -55,15 +58,15 @@
def registerSynch(synch):
"""Register an ISynchronizer.
- Synchronizers are notified at the beginning and end of
- transaction completion.
+ Synchronizers are notified about some major events in a transaction's
+ life. See ISynchronizer for details.
"""
def unregisterSynch(synch):
"""Unregister an ISynchronizer.
- Synchronizers are notified at the beginning and end of
- transaction completion.
+ Synchronizers are notified about some major events in a transaction's
+ life. See ISynchronizer for details.
"""
class ITransaction(zope.interface.Interface):
@@ -365,3 +368,10 @@
def afterCompletion(transaction):
"""Hook that is called by the transaction after completing a commit.
"""
+
+ def newTransaction(transaction):
+ """Hook that is called at the start of a transaction.
+
+ This hook is called when, and only when, a transaction manager's
+ begin() method is called explictly.
+ """
More information about the Zodb-checkins
mailing list