[Zodb-checkins] SVN: ZODB/trunk/ merge
http://svn.zope.org/ZODB/branches/anguenot-after_commit_hooks/
branch
Julien Anguenot
ja at nuxeo.com
Thu Jan 5 16:12:03 EST 2006
Log message for revision 41164:
merge http://svn.zope.org/ZODB/branches/anguenot-after_commit_hooks/ branch
Changed:
U ZODB/trunk/NEWS.txt
U ZODB/trunk/src/transaction/_transaction.py
U ZODB/trunk/src/transaction/interfaces.py
U ZODB/trunk/src/transaction/tests/test_transaction.py
-=-
Modified: ZODB/trunk/NEWS.txt
===================================================================
--- ZODB/trunk/NEWS.txt 2006-01-05 20:48:01 UTC (rev 41163)
+++ ZODB/trunk/NEWS.txt 2006-01-05 21:12:02 UTC (rev 41164)
@@ -46,3 +46,15 @@
- (3.7a1) An optimization for loading non-current data (MVCC) was
inadvertently disabled in ``_setstate()``; this has been repaired.
+
+After Commit hooks
+------------------
+
+- (3.7a1) Transaction objects have a new method,
+ ``addAfterCommitHook(hook, *args, **kws)``. Hook functions
+ registered with a transaction are called after the transaction
+ commits or aborts. For example, one might want to launch non
+ transactional or asynchrnonous code after a successful, or aborted,
+ commit. See ``test_afterCommitHook()`` in
+ ``transaction/tests/test_transaction.py`` for a tutorial doctest,
+ and the ``ITransaction`` interface for details.
Modified: ZODB/trunk/src/transaction/_transaction.py
===================================================================
--- ZODB/trunk/src/transaction/_transaction.py 2006-01-05 20:48:01 UTC (rev 41163)
+++ ZODB/trunk/src/transaction/_transaction.py 2006-01-05 21:12:02 UTC (rev 41164)
@@ -115,6 +115,20 @@
passing it a callable and arguments. The callable will be called with its
arguments at the start of the commit (but not for substransaction commits).
+After-commit hook
+------------------
+
+Sometimes, applications want to execute code after a transaction is
+committed or aborted. For example, one might want to launch non
+transactional code after a successful commit. Or still someone might
+want to launch asynchronous code after. A post-commit hook is
+available for such use cases: use addAfterCommitHook(), passing it a
+callable and arguments. The callable will be called with a Boolean
+value representing the status of the commit operation as first
+argument (true if successfull or false iff aborted) preceding its
+arguments at the start of the commit (but not for substransaction
+commits).
+
Error handling
--------------
@@ -241,6 +255,9 @@
# List of (hook, args, kws) tuples added by addBeforeCommitHook().
self._before_commit = []
+ # List of (hook, args, kws) tuples added by addAfterCommitHook().
+ self._after_commit = []
+
# Raise TransactionFailedError, due to commit()/join()/register()
# getting called when the current transaction has already suffered
# a commit/savepoint failure.
@@ -292,7 +309,7 @@
savepoint = Savepoint(self, optimistic, *self._resources)
except:
self._cleanup(self._resources)
- self._saveCommitishError() # reraises!
+ self._saveAndRaiseCommitishError() # reraises!
if self._savepoint2index is None:
self._savepoint2index = weakref.WeakKeyDictionary()
@@ -376,16 +393,19 @@
try:
self._commitResources()
+ self.status = Status.COMMITTED
except:
- self._saveCommitishError() # This raises!
-
- self.status = Status.COMMITTED
- if self._manager:
- self._manager.free(self)
- self._synchronizers.map(lambda s: s.afterCompletion(self))
+ t, v, tb = self._saveAndGetCommitishError()
+ self._callAfterCommitHooks(status=False)
+ raise t, v, tb
+ else:
+ if self._manager:
+ self._manager.free(self)
+ self._synchronizers.map(lambda s: s.afterCompletion(self))
+ self._callAfterCommitHooks(status=True)
self.log.debug("commit")
- def _saveCommitishError(self):
+ def _saveAndGetCommitishError(self):
self.status = Status.COMMITFAILED
# Save the traceback for TransactionFailedError.
ft = self._failure_traceback = StringIO()
@@ -396,6 +416,10 @@
traceback.print_tb(tb, None, ft)
# Append the exception type and value.
ft.writelines(traceback.format_exception_only(t, v))
+ return t, v, tb
+
+ def _saveAndRaiseCommitishError(self):
+ t, v, tb = self._saveAndGetCommitishError()
raise t, v, tb
def getBeforeCommitHooks(self):
@@ -421,6 +445,44 @@
hook(*args, **kws)
self._before_commit = []
+ def getAfterCommitHooks(self):
+ return iter(self._after_commit)
+
+ def addAfterCommitHook(self, hook, args=(), kws=None):
+ if kws is None:
+ kws = {}
+ self._after_commit.append((hook, tuple(args), kws))
+
+ def _callAfterCommitHooks(self, status=True):
+ # Avoid to abort anything at the end if no hooks are registred.
+ if not self._after_commit:
+ return
+ # Call all hooks registered, allowing further registrations
+ # during processing. Note that calls to addAterCommitHook() may
+ # add additional hooks while hooks are running, and iterating over a
+ # growing list is well-defined in Python.
+ for hook, args, kws in self._after_commit:
+ # The first argument passed to the hook is a Boolean value,
+ # true if the commit succeeded, or false if the commit aborted.
+ try:
+ hook(status, *args, **kws)
+ except:
+ # We need to catch the exceptions if we want all hooks
+ # to be called
+ self.log.error("Error in after commit hook exec in %s ",
+ hook, exc_info=sys.exc_info())
+ # The transaction is already committed. It must not have
+ # further effects after the commit.
+ for rm in self._resources:
+ try:
+ rm.abort(self)
+ except:
+ # XXX should we take further actions here ?
+ self.log.error("Error in abort() on manager %s",
+ rm, exc_info=sys.exc_info())
+ self._after_commit = []
+ self._before_commit = []
+
def _commitResources(self):
# Execute the two-phase commit protocol.
@@ -687,7 +749,7 @@
savepoint.rollback()
except:
# Mark the transaction as failed.
- transaction._saveCommitishError() # reraises!
+ transaction._saveAndRaiseCommitishError() # reraises!
class AbortSavepoint:
Modified: ZODB/trunk/src/transaction/interfaces.py
===================================================================
--- ZODB/trunk/src/transaction/interfaces.py 2006-01-05 20:48:01 UTC (rev 41163)
+++ ZODB/trunk/src/transaction/interfaces.py 2006-01-05 21:12:02 UTC (rev 41164)
@@ -232,6 +232,43 @@
by a top-level transaction commit.
"""
+ def addAfterCommitHook(hook, args=(), kws=None):
+ """Register a hook to call after a transaction commit attempt.
+
+ The specified hook function will be called after the transaction
+ commit succeeds or aborts. The first argument passed to the hook
+ is a Boolean value, true if the commit succeeded, or false if the
+ commit aborted. `args` specifies additional positional, and `kws`
+ keyword, arguments to pass to the hook. `args` is a sequence of
+ positional arguments to be passed, defaulting to an empty tuple
+ (only the true/false success argument is passed). `kws` is a
+ dictionary of keyword argument names and values to be passed, or
+ the default None (no keyword arguments are passed).
+
+ Multiple hooks can be registered and will be called in the order they
+ were registered (first registered, first called). This method can
+ also be called from a hook: an executing hook can register more
+ hooks. Applications should take care to avoid creating infinite loops
+ by recursively registering hooks.
+
+ Hooks are called only for a top-level commit. A subtransaction
+ commit or savepoint creation does not call any hooks. Calling a
+ hook "consumes" its registration: hook registrations do not
+ persist across transactions. If it's desired to call the same
+ hook on every transaction commit, then addAfterCommitHook() must be
+ called with that hook during every transaction; in such a case
+ consider registering a synchronizer object via a TransactionManager's
+ registerSynch() method instead.
+ """
+
+ def getAfterCommitHooks():
+ """Return iterable producing the registered addAfterCommit hooks.
+
+ A triple (hook, args, kws) is produced for each registered hook.
+ The hooks are produced in the order in which they would be invoked
+ by a top-level transaction commit.
+ """
+
class ITransactionDeprecated(zope.interface.Interface):
"""Deprecated parts of the transaction API."""
Modified: ZODB/trunk/src/transaction/tests/test_transaction.py
===================================================================
--- ZODB/trunk/src/transaction/tests/test_transaction.py 2006-01-05 20:48:01 UTC (rev 41163)
+++ ZODB/trunk/src/transaction/tests/test_transaction.py 2006-01-05 21:12:02 UTC (rev 41164)
@@ -1,6 +1,6 @@
##############################################################################
#
-# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# Copyright (c) 2001, 2002, 2005 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
@@ -11,7 +11,7 @@
# FOR A PARTICULAR PURPOSE
#
##############################################################################
-"""Test tranasction behavior for variety of cases.
+"""Test transaction behavior for variety of cases.
I wrote these unittests to investigate some odd transaction
behavior when doing unittests of integrating non sub transaction
@@ -241,7 +241,6 @@
assert self.nosub1._p_jar.ctpc_abort == 1
-
# last test, check the hosing mechanism
## def testHoserStoppage(self):
@@ -728,8 +727,270 @@
"arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec0']
>>> reset_log()
+
+ When modifing persitent objects within before commit hooks
+ modifies the objects, of course :)
+
+ Start a new transaction
+
+ >>> t = transaction.begin()
+
+ Create a DB instance and add a IOBTree within
+
+ >>> from ZODB.tests.util import DB
+ >>> from ZODB.tests.util import P
+ >>> db = DB()
+ >>> con = db.open()
+ >>> root = con.root()
+ >>> root['p'] = P('julien')
+ >>> p = root['p']
+
+ >>> p.name
+ 'julien'
+
+ This hook will get the object from the `DB` instance and change
+ the flag attribute.
+
+ >>> def hookmodify(status, arg=None, kw1='no_kw1', kw2='no_kw2'):
+ ... p.name = 'jul'
+
+ Now register this hook and commit.
+
+ >>> t.addBeforeCommitHook(hookmodify, (p, 1))
+ >>> transaction.commit()
+
+ Nothing should have changed since it should have been aborted.
+
+ >>> p.name
+ 'jul'
+
+ >>> db.close()
"""
+def test_addAfterCommitHook():
+ """Test addAfterCommitHook.
+
+ Let's define a hook to call, and a way to see that it was called.
+
+ >>> log = []
+ >>> def reset_log():
+ ... del log[:]
+
+ >>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
+ ... log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2))
+
+ Now register the hook with a transaction.
+
+ >>> import transaction
+ >>> t = transaction.begin()
+ >>> t.addAfterCommitHook(hook, '1')
+
+ We can see that the hook is indeed registered.
+
+ >>> [(hook.func_name, args, kws)
+ ... for hook, args, kws in t.getAfterCommitHooks()]
+ [('hook', ('1',), {})]
+
+ When transaction commit is done, the hook is called, with its
+ arguments.
+
+ >>> log
+ []
+ >>> t.commit()
+ >>> log
+ ["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
+ >>> reset_log()
+
+ A hook's registration is consumed whenever the hook is called. Since
+ the hook above was called, it's no longer registered:
+
+ >>> len(list(t.getAfterCommitHooks()))
+ 0
+ >>> transaction.commit()
+ >>> log
+ []
+
+ The hook is only called after a full commit, not for a savepoint or
+ subtransaction.
+
+ >>> t = transaction.begin()
+ >>> t.addAfterCommitHook(hook, 'A', dict(kw1='B'))
+ >>> dummy = t.savepoint()
+ >>> log
+ []
+ >>> t.commit(subtransaction=True)
+ >>> log
+ []
+ >>> t.commit()
+ >>> log
+ ["True arg 'A' kw1 'B' kw2 'no_kw2'"]
+ >>> reset_log()
+
+ If a transaction is aborted, no hook is called.
+
+ >>> t = transaction.begin()
+ >>> t.addAfterCommitHook(hook, ["OOPS!"])
+ >>> transaction.abort()
+ >>> log
+ []
+ >>> transaction.commit()
+ >>> log
+ []
+
+ The hook is called after the commit is done, so even if the
+ commit fails the hook will have been called. To provoke failures in
+ commit, we'll add failing resource manager to the transaction.
+
+ >>> class CommitFailure(Exception):
+ ... pass
+ >>> class FailingDataManager:
+ ... def tpc_begin(self, txn, sub=False):
+ ... raise CommitFailure
+ ... def abort(self, txn):
+ ... pass
+
+ >>> t = transaction.begin()
+ >>> t.join(FailingDataManager())
+
+ >>> t.addAfterCommitHook(hook, '2')
+ >>> t.commit()
+ Traceback (most recent call last):
+ ...
+ CommitFailure
+ >>> log
+ ["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
+ >>> reset_log()
+
+ Let's register several hooks.
+
+ >>> t = transaction.begin()
+ >>> t.addAfterCommitHook(hook, '4', dict(kw1='4.1'))
+ >>> t.addAfterCommitHook(hook, '5', dict(kw2='5.2'))
+
+ They are returned in the same order by getAfterCommitHooks.
+
+ >>> [(hook.func_name, args, kws) #doctest: +NORMALIZE_WHITESPACE
+ ... for hook, args, kws in t.getAfterCommitHooks()]
+ [('hook', ('4',), {'kw1': '4.1'}),
+ ('hook', ('5',), {'kw2': '5.2'})]
+
+ And commit also calls them in this order.
+
+ >>> t.commit()
+ >>> len(log)
+ 2
+ >>> log #doctest: +NORMALIZE_WHITESPACE
+ ["True arg '4' kw1 '4.1' kw2 'no_kw2'",
+ "True arg '5' kw1 'no_kw1' kw2 '5.2'"]
+ >>> reset_log()
+
+ While executing, a hook can itself add more hooks, and they will all
+ be called before the real commit starts.
+
+ >>> def recurse(status, txn, arg):
+ ... log.append('rec' + str(arg))
+ ... if arg:
+ ... txn.addAfterCommitHook(hook, '-')
+ ... txn.addAfterCommitHook(recurse, (txn, arg-1))
+
+ >>> t = transaction.begin()
+ >>> t.addAfterCommitHook(recurse, (t, 3))
+ >>> transaction.commit()
+ >>> log #doctest: +NORMALIZE_WHITESPACE
+ ['rec3',
+ "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+ 'rec2',
+ "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+ 'rec1',
+ "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+ 'rec0']
+ >>> reset_log()
+
+ If an after commit hook is raising an exception then it will log a
+ message at error level so that if other hooks are registered they
+ can be executed. We don't support execution dependencies at this level.
+
+ >>> mgr = transaction.TransactionManager()
+ >>> do = DataObject(mgr)
+
+ >>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
+ ... raise TypeError("Fake raise")
+
+ >>> t = transaction.begin()
+
+ >>> t.addAfterCommitHook(hook, ('-', 1))
+ >>> t.addAfterCommitHook(hookRaise, ('-', 2))
+ >>> t.addAfterCommitHook(hook, ('-', 3))
+ >>> transaction.commit()
+
+ >>> log
+ ["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"]
+
+ >>> reset_log()
+
+ Test that the associated transaction manager has been cleanup when
+ after commit hooks are registered
+
+ >>> mgr = transaction.TransactionManager()
+ >>> do = DataObject(mgr)
+
+ >>> t = transaction.begin()
+ >>> len(t._manager._txns)
+ 1
+
+ >>> t.addAfterCommitHook(hook, ('-', 1))
+ >>> transaction.commit()
+
+ >>> log
+ ["True arg '-' kw1 1 kw2 'no_kw2'"]
+
+ >>> len(t._manager._txns)
+ 0
+
+ >>> reset_log()
+
+
+ The transaction is already committed when the after commit hooks
+ will be executed. Executing the hooks must not have further
+ effects on persistent objects.
+
+ Start a new transaction
+
+ >>> t = transaction.begin()
+
+ Create a DB instance and add a IOBTree within
+
+ >>> from ZODB.tests.util import DB
+ >>> from ZODB.tests.util import P
+ >>> db = DB()
+ >>> con = db.open()
+ >>> root = con.root()
+ >>> root['p'] = P('julien')
+ >>> p = root['p']
+
+ >>> p.name
+ 'julien'
+
+ This hook will get the object from the `DB` instance and change
+ the flag attribute.
+
+ >>> def badhook(status, arg=None, kw1='no_kw1', kw2='no_kw2'):
+ ... p.name = 'jul'
+
+ Now register this hook and commit.
+
+ >>> t.addAfterCommitHook(badhook, (p, 1))
+ >>> transaction.commit()
+
+ Nothing should have changed since it should have been aborted.
+
+ >>> p.name
+ 'julien'
+
+ >>> db.close()
+
+ """
+
def test_suite():
from zope.testing.doctest import DocTestSuite
return unittest.TestSuite((
More information about the Zodb-checkins
mailing list