[Zodb-checkins] SVN: ZODB/trunk/src/transaction/ Merge anguenot-ordering-beforecommitsubscribers branch.

Tim Peters tim.one at comcast.net
Fri Aug 12 20:41:44 EDT 2005


Log message for revision 37908:
  Merge anguenot-ordering-beforecommitsubscribers branch.
  
  addBeforeCommitHook() is new, a generalization of the
  now-deprecated beforeCommitHook() that allows influencing
  the order in which commit hooks are called.
  

Changed:
  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/src/transaction/_transaction.py
===================================================================
--- ZODB/trunk/src/transaction/_transaction.py	2005-08-12 23:10:38 UTC (rev 37907)
+++ ZODB/trunk/src/transaction/_transaction.py	2005-08-13 00:41:44 UTC (rev 37908)
@@ -105,13 +105,13 @@
 call.
 
 Before-commit hook
----------------
+------------------
 
 Sometimes, applications want to execute some code when a transaction is
 committed.  For example, one might want to delay object indexing until a
 transaction commits, rather than indexing every time an object is changed.
 Or someone might want to check invariants only after a set of operations.  A
-pre-commit hook is available for such use cases, just use beforeCommitHook()
+pre-commit hook is available for such use cases:  use addBeforeCommitHook(),
 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).
 
@@ -146,6 +146,7 @@
 as their only argument.
 """
 
+import bisect
 import logging
 import sys
 import thread
@@ -238,10 +239,16 @@
         # raised, incorporating this traceback.
         self._failure_traceback = None
 
-        # Holds (hook, args, kws) triples added by beforeCommitHook.
-        # TODO:  in Python 2.4, change to collections.deque; lists can be
+        # List of (order, index, hook, args, kws) tuples added by
+        # addbeforeCommitHook().  `index` is used to resolve ties on equal
+        # `order` values, preserving the order in which the hooks were
+        # registered.  Each time we append a tuple to _before_commit,
+        # the current value of _before_commit_index is used for the
+        # index, and then the latter is incremented by 1.
+        # TODO: in Python 2.4, change to collections.deque; lists can be
         # inefficient for FIFO access of this kind.
         self._before_commit = []
+        self._before_commit_index = 0
 
     # Raise TransactionFailedError, due to commit()/join()/register()
     # getting called when the current transaction has already suffered
@@ -408,17 +415,34 @@
         raise t, v, tb
 
     def getBeforeCommitHooks(self):
-        return iter(self._before_commit)
+        # Don't return the hook order and index values because of
+        # backward compatibility, and because they're internal details.
+        return iter([x[2:] for x in self._before_commit])
 
+    def addBeforeCommitHook(self, hook, args=(), kws=None, order=0):
+        if not isinstance(order, int):
+            raise ValueError("An integer value is required "
+                             "for the order argument")
+        if kws is None:
+            kws = {}
+        bisect.insort(self._before_commit, (order, self._before_commit_index,
+                                            hook, tuple(args), kws))
+        self._before_commit_index += 1
+
     def beforeCommitHook(self, hook, *args, **kws):
-        self._before_commit.append((hook, args, kws))
+        from ZODB.utils import deprecated37
 
+        deprecated37("Use addBeforeCommitHook instead of beforeCommitHook.")
+        # Default order is zero.
+        self.addBeforeCommitHook(hook, args, kws, order=0)
+
     def _callBeforeCommitHooks(self):
         # Call all hooks registered, allowing further registrations
         # during processing.
         while self._before_commit:
-            hook, args, kws = self._before_commit.pop(0)
+            order, index, hook, args, kws = self._before_commit.pop(0)
             hook(*args, **kws)
+        self._before_commit_index = 0
 
     def _commitResources(self):
         # Execute the two-phase commit protocol.

Modified: ZODB/trunk/src/transaction/interfaces.py
===================================================================
--- ZODB/trunk/src/transaction/interfaces.py	2005-08-12 23:10:38 UTC (rev 37907)
+++ ZODB/trunk/src/transaction/interfaces.py	2005-08-13 00:41:44 UTC (rev 37908)
@@ -167,9 +167,12 @@
         raise an exception, or remove `<name, value>` pairs).
         """
 
-    def beforeCommitHook(hook, *args, **kws):
+    # deprecated37
+    def beforeCommitHook(__hook, *args, **kws):
         """Register a hook to call before the transaction is committed.
 
+        THIS IS DEPRECATED IN ZODB 3.5.  Use addBeforeCommitHook() instead.
+
         The specified hook function will be called after the transaction's
         commit method has been called, but before the commit process has been
         started.  The hook will be passed the specified positional and keyword
@@ -192,11 +195,51 @@
         instead.
         """
 
+    def addBeforeCommitHook(hook, args=(), kws=None, order=0):
+        """Register a hook to call before the transaction is committed.
+
+        The specified hook function will be called after the transaction's
+        commit method has been called, but before the commit process has been
+        started.  The hook will be passed the specified positional (`args`)
+        and keyword (`kws`) arguments.  `args` is a sequence of positional
+        arguments to be passed, defaulting to an empty tuple (no positional
+        arguments are 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), except that
+        hooks registered with different `order` arguments are invoked from
+        smallest `order` value to largest.  `order` must be an integer,
+        and defaults to 0.
+
+        For instance, a hook registered with order=1 will be invoked after
+        another hook registered with order=-1 and before another registered
+        with order=2, regardless of which was registered first.  When two
+        hooks are registered with the same order, the first one registered is
+        called first.
+
+        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.  If the
+        transaction is aborted, hooks are not called, and are discarded.
+        Calling a hook "consumes" its registration too:  hook registrations
+        do not persist across transactions.  If it's desired to call the same
+        hook on every transaction commit, then addBeforeCommitHook() 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 getBeforeCommitHooks():
-        """Return iterable producing the registered beforeCommit hooks.
+        """Return iterable producing the registered addBeforeCommit hooks.
 
         A triple (hook, args, kws) is produced for each registered hook.
-        The hooks are produced in the order in which they were registered.
+        The hooks are produced in the order in which they would be invoked
+        by a top-level transaction commit.
         """
 
 class ITransactionDeprecated(zope.interface.Interface):

Modified: ZODB/trunk/src/transaction/tests/test_transaction.py
===================================================================
--- ZODB/trunk/src/transaction/tests/test_transaction.py	2005-08-12 23:10:38 UTC (rev 37907)
+++ ZODB/trunk/src/transaction/tests/test_transaction.py	2005-08-13 00:41:44 UTC (rev 37908)
@@ -44,6 +44,7 @@
 
 import transaction
 from ZODB.utils import positive_id
+from ZODB.tests.warnhook import WarningsHook
 
 # deprecated37  remove when subtransactions go away
 # Don't complain about subtxns in these tests.
@@ -410,8 +411,34 @@
 
     """
 
+def hook():
+    pass
+
+class BeforeCommitHookTests(unittest.TestCase):
+
+    def test_01_beforecommithook_order_exceptions(self):
+        # string
+        t = transaction.Transaction()
+        self.assertRaises(ValueError, t.addBeforeCommitHook,
+                          hook, order='string')
+
+    def test_02_beforecommithook_order_exceptions(self):
+        # float
+        t = transaction.Transaction()
+        self.assertRaises(ValueError, t.addBeforeCommitHook,
+                          hook, order=1.2)
+
+    def test_03_beforecommithook_order_exceptions(self):
+        # object
+        t = transaction.Transaction()
+        class foo:
+            pass
+        self.assertRaises(ValueError, t.addBeforeCommitHook,
+                          hook, order=foo())
+
+# deprecated37; remove this then
 def test_beforeCommitHook():
-    """Test the beforeCommitHook.
+    """Test beforeCommitHook.
 
     Let's define a hook to call, and a way to see that it was called.
 
@@ -422,12 +449,37 @@
       >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
       ...     log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))
 
+    beforeCommitHook is deprecated, so we need cruft to suppress the
+    warnings.
+
+      >>> whook = WarningsHook()
+      >>> whook.install()
+
+    Fool the warnings module into delivering the warnings despite that
+    they've been seen before; this is needed in case this test is run
+    more than once.
+
+      >>> import warnings
+      >>> warnings.filterwarnings("always", category=DeprecationWarning)
+
     Now register the hook with a transaction.
 
       >>> import transaction
       >>> t = transaction.begin()
       >>> t.beforeCommitHook(hook, '1')
 
+    Make sure it triggered a deprecation warning:
+
+      >>> len(whook.warnings)
+      1
+      >>> message, category, filename, lineno = whook.warnings[0]
+      >>> print message
+      This will be removed in ZODB 3.7:
+      Use addBeforeCommitHook instead of beforeCommitHook.
+      >>> category.__name__
+      'DeprecationWarning'
+      >>> whook.clear()
+
     We can see that the hook is indeed registered.
 
       >>> [(hook.func_name, args, kws)
@@ -548,15 +600,245 @@
                "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
        'rec0']
       >>> reset_log()
+
+    We have to uninstall the warnings hook so that other warnings don't get
+    lost.
+
+      >>> whook.uninstall()
+
+    Obscure:  There is no API call for removing the filter we added, but
+    filters appears to be a public variable.
+
+      >>> del warnings.filters[0]
     """
 
+def test_addBeforeCommitHook():
+    """Test addBeforeCommitHook, without order arguments.
+
+    Let's define a hook to call, and a way to see that it was called.
+
+      >>> log = []
+      >>> def reset_log():
+      ...     del log[:]
+
+      >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
+      ...     log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))
+
+    Now register the hook with a transaction.
+
+      >>> import transaction
+      >>> t = transaction.begin()
+      >>> t.addBeforeCommitHook(hook, '1')
+
+    We can see that the hook is indeed registered.
+
+      >>> [(hook.func_name, args, kws)
+      ...  for hook, args, kws in t.getBeforeCommitHooks()]
+      [('hook', ('1',), {})]
+
+    When transaction commit starts, the hook is called, with its
+    arguments.
+
+      >>> log
+      []
+      >>> t.commit()
+      >>> log
+      ["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.getBeforeCommitHooks()))
+      0
+      >>> transaction.commit()
+      >>> log
+      []
+
+    The hook is only called for a full commit, not for a savepoint or
+    subtransaction.
+
+      >>> t = transaction.begin()
+      >>> t.addBeforeCommitHook(hook, 'A', dict(kw1='B'))
+      >>> dummy = t.savepoint()
+      >>> log
+      []
+      >>> t.commit(subtransaction=True)
+      >>> log
+      []
+      >>> t.commit()
+      >>> log
+      ["arg 'A' kw1 'B' kw2 'no_kw2'"]
+      >>> reset_log()
+
+    If a transaction is aborted, no hook is called.
+
+      >>> t = transaction.begin()
+      >>> t.addBeforeCommitHook(hook, ["OOPS!"])
+      >>> transaction.abort()
+      >>> log
+      []
+      >>> transaction.commit()
+      >>> log
+      []
+
+    The hook is called before the commit does anything, 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.addBeforeCommitHook(hook, '2')
+      >>> t.commit()
+      Traceback (most recent call last):
+      ...
+      CommitFailure
+      >>> log
+      ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
+      >>> reset_log()
+
+    Let's register several hooks.
+
+      >>> t = transaction.begin()
+      >>> t.addBeforeCommitHook(hook, '4', dict(kw1='4.1'))
+      >>> t.addBeforeCommitHook(hook, '5', dict(kw2='5.2'))
+
+    They are returned in the same order by getBeforeCommitHooks.
+
+      >>> [(hook.func_name, args, kws)     #doctest: +NORMALIZE_WHITESPACE
+      ...  for hook, args, kws in t.getBeforeCommitHooks()]
+      [('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
+      ["arg '4' kw1 '4.1' kw2 'no_kw2'",
+       "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(txn, arg):
+      ...     log.append('rec' + str(arg))
+      ...     if arg:
+      ...         txn.addBeforeCommitHook(hook, '-')
+      ...         txn.addBeforeCommitHook(recurse, (txn, arg-1))
+
+      >>> t = transaction.begin()
+      >>> t.addBeforeCommitHook(recurse, (t, 3))
+      >>> transaction.commit()
+      >>> log  #doctest: +NORMALIZE_WHITESPACE
+      ['rec3',
+               "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+       'rec2',
+               "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+       'rec1',
+               "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+       'rec0']
+      >>> reset_log()
+    """
+
+def test_addBeforeCommitHookOrder():
+    """Test addBeforeCommitHook with order arguments.
+
+    Register a hook with an order explicitly equal to 0 (the default value):
+
+      >>> import transaction
+      >>> t = transaction.begin()
+      >>> t.addBeforeCommitHook(hook, '1', order=0)
+
+    We can see that the hook is indeed registered.
+
+      >>> [(hook.func_name, args, kws)
+      ...  for hook, args, kws in t.getBeforeCommitHooks()]
+      [('hook', ('1',), {})]
+
+    Let's add another one with a smaller order. It will be registered
+    to be called first.
+
+      >>> t.addBeforeCommitHook(hook, '2', order=-999999)
+      >>> [(hook.func_name, args, kws)
+      ...  for hook, args, kws in t.getBeforeCommitHooks()]
+      [('hook', ('2',), {}), ('hook', ('1',), {})]
+
+    Let's add another one with a bigger order.  It will be registered
+    to be called last.
+
+      >>> t.addBeforeCommitHook(hook, '3', order=999999)
+      >>> for hook, args, kws in t.getBeforeCommitHooks():
+      ...     print (hook.func_name, args, kws)
+      ('hook', ('2',), {})
+      ('hook', ('1',), {})
+      ('hook', ('3',), {})
+
+    Above, we checked that the order parameter works as expected.
+    Now check that insertion with the same order values  respects the order
+    of registration.
+
+      >>> t.addBeforeCommitHook(hook, '4') # order=0 implied
+      >>> for hook, args, kws in t.getBeforeCommitHooks():
+      ...     print (hook.func_name, args, kws)
+      ('hook', ('2',), {})
+      ('hook', ('1',), {})
+      ('hook', ('4',), {})
+      ('hook', ('3',), {})
+
+      >>> t.addBeforeCommitHook(hook, '5', order=999999)
+      >>> for hook, args, kws in t.getBeforeCommitHooks():
+      ...     print (hook.func_name, args, kws)
+      ('hook', ('2',), {})
+      ('hook', ('1',), {})
+      ('hook', ('4',), {})
+      ('hook', ('3',), {})
+      ('hook', ('5',), {})
+
+      >>> t.addBeforeCommitHook(hook, '6', order=-999999)
+      >>> for hook, args, kws in t.getBeforeCommitHooks():
+      ...     print (hook.func_name, args, kws)
+      ('hook', ('2',), {})
+      ('hook', ('6',), {})
+      ('hook', ('1',), {})
+      ('hook', ('4',), {})
+      ('hook', ('3',), {})
+      ('hook', ('5',), {})
+
+      >>> def hook2():
+      ...     pass
+
+      >>> t.addBeforeCommitHook(hook2, '8', order=0)
+      >>> for hook, args, kws in t.getBeforeCommitHooks():
+      ...     print (hook.func_name, args, kws)
+      ('hook', ('2',), {})
+      ('hook', ('6',), {})
+      ('hook', ('1',), {})
+      ('hook', ('4',), {})
+      ('hook2', ('8',), {})
+      ('hook', ('3',), {})
+      ('hook', ('5',), {})
+
+    """
+
 def test_suite():
     from zope.testing.doctest import DocTestSuite
     return unittest.TestSuite((
         DocTestSuite(),
         unittest.makeSuite(TransactionTests),
+        unittest.makeSuite(BeforeCommitHookTests),
         ))
 
-
 if __name__ == '__main__':
     unittest.TextTestRunner().run(test_suite())



More information about the Zodb-checkins mailing list