[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