[Zodb-checkins] SVN: ZODB/branches/3.4/ Merge ZODB/branches/efge-beforeCommitHook.

Tim Peters tim.one at comcast.net
Thu Apr 7 19:25:25 EDT 2005


Log message for revision 29901:
  Merge ZODB/branches/efge-beforeCommitHook.
  
  This is Florent Guillaume's branch, giving transaction objects
  a new beforeCommitHook() method, as proposed by Jim Fulton on
  zodb-dev.  Some changes were made to the branch code (comments,
  more tests, more words in the docs).
  

Changed:
  U   ZODB/branches/3.4/NEWS.txt
  U   ZODB/branches/3.4/src/transaction/_transaction.py
  U   ZODB/branches/3.4/src/transaction/interfaces.py
  U   ZODB/branches/3.4/src/transaction/tests/test_transaction.py

-=-
Modified: ZODB/branches/3.4/NEWS.txt
===================================================================
--- ZODB/branches/3.4/NEWS.txt	2005-04-07 19:58:42 UTC (rev 29900)
+++ ZODB/branches/3.4/NEWS.txt	2005-04-07 23:25:24 UTC (rev 29901)
@@ -2,6 +2,19 @@
 ==========================
 Release date: MM-DDD-2005
 
+transaction
+-----------
+
+Transaction objects have a new method,
+``beforeCommitHook(hook, *args, **kws)``.  Hook functions registered with
+a transaction are called at the start of a top-level commit, before any
+of the work is begun, so a hook function can perform any database operations
+it likes.  See ``test_beforeCommitHook()`` in
+``transaction/tests/test_transaction.py`` for a tutorial doctest, and
+the ``ITransaction`` interface for details.  Thanks to Florent Guillaume
+for contributing code and tests.
+
+
 Tests
 -----
 

Modified: ZODB/branches/3.4/src/transaction/_transaction.py
===================================================================
--- ZODB/branches/3.4/src/transaction/_transaction.py	2005-04-07 19:58:42 UTC (rev 29900)
+++ ZODB/branches/3.4/src/transaction/_transaction.py	2005-04-07 23:25:24 UTC (rev 29901)
@@ -100,6 +100,17 @@
 commit will start with a commit_sub() call instead of a tpc_begin()
 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()
+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).
+
 Error handling
 --------------
 
@@ -209,6 +220,11 @@
         # 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
+        # inefficient for FIFO access of this kind.
+        self._before_commit = []
+
     # Raise TransactionFailedError, due to commit()/join()/register()
     # getting called when the current transaction has already suffered
     # a commit failure.
@@ -286,6 +302,9 @@
         if self.status is Status.COMMITFAILED:
             self._prior_commit_failed() # doesn't return
 
+        if not subtransaction:
+            self._callBeforeCommitHooks()
+
         if not subtransaction and self._sub and self._resources:
             # This commit is for a top-level transaction that has
             # previously committed subtransactions.  Do one last
@@ -321,6 +340,16 @@
             self._synchronizers.map(lambda s: s.afterCompletion(self))
             self.log.debug("commit")
 
+    def beforeCommitHook(self, hook, *args, **kws):
+        self._before_commit.append((hook, args, kws))
+
+    def _callBeforeCommitHooks(self):
+        # Call all hooks registered, allowing further registrations
+        # during processing.
+        while self._before_commit:
+            hook, args, kws = self._before_commit.pop(0)
+            hook(*args, **kws)
+
     def _commitResources(self, subtransaction):
         # Execute the two-phase commit protocol.
 

Modified: ZODB/branches/3.4/src/transaction/interfaces.py
===================================================================
--- ZODB/branches/3.4/src/transaction/interfaces.py	2005-04-07 19:58:42 UTC (rev 29900)
+++ ZODB/branches/3.4/src/transaction/interfaces.py	2005-04-07 23:25:24 UTC (rev 29901)
@@ -107,7 +107,7 @@
         database is not expected to maintain consistency; it's a
         serious error.
 
-        It's important that the storage calls the passed function 
+        It's important that the storage calls the passed function
         while it still has its lock.  We don't want another thread
         to be able to read any updated data until we've had a chance
         to send an invalidation message to all of the other
@@ -131,7 +131,7 @@
         the transaction commits.
 
         This includes conflict detection and handling. If no conflicts or
-        errors occur it saves the objects in the storage. 
+        errors occur it saves the objects in the storage.
         """
 
     def abort(transaction):
@@ -139,8 +139,8 @@
 
         Abort must be called outside of a two-phase commit.
 
-        Abort is called by the transaction manager to abort transactions 
-        that are not yet in a two-phase commit. 
+        Abort is called by the transaction manager to abort transactions
+        that are not yet in a two-phase commit.
         """
 
     def sortKey():
@@ -245,7 +245,31 @@
         # Unsure:  is this allowed to cause an exception here, during
         # the two-phase commit, or can it toss data silently?
 
+    def beforeCommitHook(hook, *args, **kws):
+        """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 and keyword
+        arguments.
+
+        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 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 beforeCommitHook() 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.
+        """
+
 class IRollback(zope.interface.Interface):
 
     def rollback():

Modified: ZODB/branches/3.4/src/transaction/tests/test_transaction.py
===================================================================
--- ZODB/branches/3.4/src/transaction/tests/test_transaction.py	2005-04-07 19:58:42 UTC (rev 29900)
+++ ZODB/branches/3.4/src/transaction/tests/test_transaction.py	2005-04-07 23:25:24 UTC (rev 29901)
@@ -631,6 +631,124 @@
 
     """
 
+def test_beforeCommitHook():
+    """Test the beforeCommitHook.
+
+    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.beforeCommitHook(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:
+
+      >>> transaction.commit()
+      >>> log
+      []
+
+    The hook is only called for a full commit, not for subtransactions.
+
+      >>> t = transaction.begin()
+      >>> t.beforeCommitHook(hook, 'A', kw1='B')
+      >>> 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.beforeCommitHook(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.beforeCommitHook(hook, '2')
+      >>> t.commit()
+      Traceback (most recent call last):
+      ...
+      CommitFailure
+      >>> log
+      ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
+      >>> reset_log()
+
+    If several hooks are defined, they are called in order.
+
+      >>> t = transaction.begin()
+      >>> t.beforeCommitHook(hook, '4', kw1='4.1')
+      >>> t.beforeCommitHook(hook, '5', kw2='5.2')
+      >>> 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.beforeCommitHook(hook, '-')
+      ...         txn.beforeCommitHook(recurse, txn, arg-1)
+
+      >>> t = transaction.begin()
+      >>> t.beforeCommitHook(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_suite():
     from zope.testing.doctest import DocTestSuite
     return unittest.TestSuite((



More information about the Zodb-checkins mailing list