[Zodb-checkins]
SVN: ZODB/branches/anguenot-after_commit_hooks/src/transaction/
First attempt to implement the after commit hooks.
Julien Anguenot
ja at nuxeo.com
Tue Dec 20 11:21:54 EST 2005
Log message for revision 40910:
First attempt to implement the after commit hooks.
What needs to be done :
- Check the _callAfterCommitHooks() and the way the transaction is
aborted at the end of the calls.
- Some more tests at transaction level showing that an after commit
hooks can't have any effect on persistent objects (tried this on Zope2 myself)
Changed:
U ZODB/branches/anguenot-after_commit_hooks/src/transaction/_transaction.py
U ZODB/branches/anguenot-after_commit_hooks/src/transaction/interfaces.py
U ZODB/branches/anguenot-after_commit_hooks/src/transaction/tests/test_transaction.py
-=-
Modified: ZODB/branches/anguenot-after_commit_hooks/src/transaction/_transaction.py
===================================================================
--- ZODB/branches/anguenot-after_commit_hooks/src/transaction/_transaction.py 2005-12-20 15:56:38 UTC (rev 40909)
+++ ZODB/branches/anguenot-after_commit_hooks/src/transaction/_transaction.py 2005-12-20 16:21:52 UTC (rev 40910)
@@ -115,6 +115,19 @@
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 some code after a transaction
+is committed. For example, one might want to launch non transactional
+code after a successful commit. Or someone might want to launch
+asynchronous. 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
--------------
@@ -178,7 +191,7 @@
COMMITTING = "Committing"
COMMITTED = "Committed"
-
+
# commit() or commit(True) raised an exception. All further attempts
# to commit or join this transaction will raise TransactionFailedError.
COMMITFAILED = "Commit failed"
@@ -241,6 +254,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.
@@ -376,16 +392,21 @@
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._getCommitishError()
+ # XXX should we catch the exceptions ?
+ self._callAfterCommitHooks(status=False)
+ raise t, v, tb
+ else:
+ if self._manager:
+ self._manager.free(self)
+ self._synchronizers.map(lambda s: s.afterCompletion(self))
+ # XXX should we catch the exceptions ?
+ self._callAfterCommitHooks(status=True)
self.log.debug("commit")
- def _saveCommitishError(self):
+ def _getCommitishError(self):
self.status = Status.COMMITFAILED
# Save the traceback for TransactionFailedError.
ft = self._failure_traceback = StringIO()
@@ -396,6 +417,11 @@
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 _saveCommitishError(self):
+ # XXX this should probably
+ t, v, tb = self._getCommitishError()
raise t, v, tb
def getBeforeCommitHooks(self):
@@ -421,6 +447,41 @@
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):
+ # 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.
+ args = (status,) + args
+ # XXX should we catch exceptions ? or at commit() level ?
+ hook(*args, **kws)
+ self._after_commit = []
+ # The transaction is already committed. It must not have
+ # further effects after the commit.
+ for rm in self._resources:
+ if hasattr(rm, 'objects'):
+ # `MultiObjectRessourceAdapter` instance
+ # XXX I'm not sure if this is enough ?
+ rm.objects = []
+ else:
+ # `Connection` instance
+ # XXX this has side effects on third party code tests that
+ # try to introspect the aborted objects.
+ rm.abort(self)
+ self._before_commit = []
+ # XXX do we need to cleanup some more ?
+
def _commitResources(self):
# Execute the two-phase commit protocol.
Modified: ZODB/branches/anguenot-after_commit_hooks/src/transaction/interfaces.py
===================================================================
--- ZODB/branches/anguenot-after_commit_hooks/src/transaction/interfaces.py 2005-12-20 15:56:38 UTC (rev 40909)
+++ ZODB/branches/anguenot-after_commit_hooks/src/transaction/interfaces.py 2005-12-20 16:21:52 UTC (rev 40910)
@@ -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/branches/anguenot-after_commit_hooks/src/transaction/tests/test_transaction.py
===================================================================
--- ZODB/branches/anguenot-after_commit_hooks/src/transaction/tests/test_transaction.py 2005-12-20 15:56:38 UTC (rev 40909)
+++ ZODB/branches/anguenot-after_commit_hooks/src/transaction/tests/test_transaction.py 2005-12-20 16:21:52 UTC (rev 40910)
@@ -730,6 +730,153 @@
>>> reset_log()
"""
+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()
+
+ The transaction is already committed when the after commit hooks
+ will be executed. Executing the hooks must not have further
+ effects.
+
+ TODO
+
+ """
+
def test_suite():
from zope.testing.doctest import DocTestSuite
return unittest.TestSuite((
More information about the Zodb-checkins
mailing list