Synchronizers

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 transaction, 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()

One more, very obscure. It was the case that if the first action a new threaded transaction manager saw was a begin() call, then synchronizers registered after that in the same transaction weren't communicated to the Transaction object, and so the synchronizers' afterCompletion() hooks weren't called when the transaction commited. None of the test suites (ZODB's, Zope 2.8's, or Zope3's) caught that, but apparently Zope 3 takes this path at some point when serving pages.

>>> tm = transaction.ThreadTransactionManager()
>>> st.sync_called = False
>>> dummy = tm.begin()  # we're doing this _before_ opening a connection
>>> cn = db.open(transaction_manager=tm)
>>> rt = cn.root()      # make a change
>>> rt['c'] = 3
>>> st.sync_called
False

Now ensure that cn.afterCompletion() -> st.sync() gets called by commit despite that the Connection registered after the transaction began:

>>> tm.commit()
>>> st.sync_called
True

And try the same thing with a non-threaded transaction manager:

>>> cn.close()
>>> tm = transaction.TransactionManager()
>>> st.sync_called = False
>>> dummy = tm.begin()  # we're doing this _before_ opening a connection
>>> cn = db.open(transaction_manager=tm)
>>> rt = cn.root()      # make a change
>>> rt['d'] = 4
>>> st.sync_called
False
>>> tm.commit()
>>> st.sync_called
True
>>> cn.close()
>>> db.close()