[Checkins] SVN: transaction/trunk/ New Features:

Jim Fulton jim at zope.com
Wed May 12 16:36:53 EDT 2010


Log message for revision 112264:
  New Features:
  
  - Transaction managers and the transaction module can be used with the
    with statement to define transaction boundaries, as in::
  
  with transaction:
           ... do some things ...
  
  See transaction/tests/convenience.txt for more details.
  
  - There is a new iterator function that automates dealing with
    transient errors (such as ZODB confict errors). For example, in::
  
  for attempt in transaction.attempts(5):
           with attempt:
               ... do some things ..
  
  If the work being done raises transient errors, the transaction will
    be retried up to 5 times.
  
  See transaction/tests/convenience.txt for more details.
  

Changed:
  U   transaction/trunk/CHANGES.txt
  U   transaction/trunk/transaction/__init__.py
  U   transaction/trunk/transaction/_manager.py
  U   transaction/trunk/transaction/interfaces.py
  A   transaction/trunk/transaction/tests/convenience.txt
  U   transaction/trunk/transaction/tests/savepointsample.py
  U   transaction/trunk/transaction/tests/test_transaction.py

-=-
Modified: transaction/trunk/CHANGES.txt
===================================================================
--- transaction/trunk/CHANGES.txt	2010-05-12 18:24:18 UTC (rev 112263)
+++ transaction/trunk/CHANGES.txt	2010-05-12 20:36:53 UTC (rev 112264)
@@ -2,10 +2,33 @@
 =======
 
 1.1.0 (1010-05-??)
+------------------
 
+New Features:
+
+- Transaction managers and the transaction module can be used with the
+  with statement to define transaction boundaries, as in::
+
+     with transaction:
+         ... do some things ...
+
+  See transaction/tests/convenience.txt for more details.
+
+- There is a new iterator function that automates dealing with
+  transient errors (such as ZODB confict errors). For example, in::
+
+     for attempt in transaction.attempts(5):
+         with attempt:
+             ... do some things ..
+
+  If the work being done raises transient errors, the transaction will
+  be retried up to 5 times.
+
+  See transaction/tests/convenience.txt for more details.
+
 Bugs fixed:
-=======
 
+
 - Fixed a bug that caused extra commit calls to be made on data
   managers under certain special circumstances.
 

Modified: transaction/trunk/transaction/__init__.py
===================================================================
--- transaction/trunk/transaction/__init__.py	2010-05-12 18:24:18 UTC (rev 112263)
+++ transaction/trunk/transaction/__init__.py	2010-05-12 20:36:53 UTC (rev 112264)
@@ -21,10 +21,12 @@
 from transaction._manager import ThreadTransactionManager
 
 manager = ThreadTransactionManager()
-get = manager.get
+get = __enter__ = manager.get
 begin = manager.begin
 commit = manager.commit
 abort = manager.abort
+__exit__ = manager.__exit__
 doom = manager.doom
 isDoomed = manager.isDoomed
 savepoint = manager.savepoint
+attempts = manager.attempts

Modified: transaction/trunk/transaction/_manager.py
===================================================================
--- transaction/trunk/transaction/_manager.py	2010-05-12 18:24:18 UTC (rev 112263)
+++ transaction/trunk/transaction/_manager.py	2010-05-12 20:36:53 UTC (rev 112264)
@@ -17,12 +17,15 @@
 are associated with the right transaction.
 """
 
+from transaction.weakset import WeakSet
+from transaction._transaction import Transaction
+from transaction.interfaces import TransientError
+
 import thread
 
-from transaction.weakset import WeakSet
 
-from transaction._transaction import Transaction
 
+
 # Used for deprecated arguments.  ZODB.utils.DEPRECATED_ARGUMENT was
 # too hard to use here, due to the convoluted import dance across
 # __init__.py files.
@@ -55,6 +58,7 @@
 # so that Transactions "see" synchronizers that get registered after the
 # Transaction object is constructed.
 
+
 class TransactionManager(object):
 
     def __init__(self):
@@ -68,6 +72,8 @@
         _new_transaction(txn, self._synchs)
         return txn
 
+    __enter__ = lambda self: self.begin()
+
     def get(self):
         if self._txn is None:
             self._txn = Transaction(self._synchs, self)
@@ -95,9 +101,34 @@
     def abort(self):
         return self.get().abort()
 
+    def __exit__(self, t, v, tb):
+        if v is None:
+            self.commit()
+        else:
+            self.abort()
+
     def savepoint(self, optimistic=False):
         return self.get().savepoint(optimistic)
 
+    def attempts(self, number=3):
+        assert number > 0
+        while number:
+            number -= 1
+            if number:
+                yield Attempt(self)
+            else:
+                yield self
+
+    def _retryable(self, error_type, error):
+        if issubclass(error_type, TransientError):
+            return True
+
+        for dm in self.get()._resources:
+            should_retry = getattr(dm, 'should_retry', None)
+            if (should_retry is not None) and should_retry(error):
+                return True
+
+
 class ThreadTransactionManager(TransactionManager):
     """Thread-aware transaction manager.
 
@@ -153,3 +184,19 @@
         tid = thread.get_ident()
         ws = self._synchs[tid]
         ws.remove(synch)
+
+class Attempt(object):
+
+    def __init__(self, manager):
+        self.manager = manager
+
+    def __enter__(self):
+        return self.manager.__enter__()
+
+    def __exit__(self, t, v, tb):
+        if v is None:
+            self.manager.commit()
+        else:
+            retry = self.manager._retryable(t, v)
+            self.manager.abort()
+            return retry

Modified: transaction/trunk/transaction/interfaces.py
===================================================================
--- transaction/trunk/transaction/interfaces.py	2010-05-12 18:24:18 UTC (rev 112263)
+++ transaction/trunk/transaction/interfaces.py	2010-05-12 20:36:53 UTC (rev 112264)
@@ -11,11 +11,7 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-"""Transaction Interfaces
 
-$Id$
-"""
-
 import zope.interface
 
 class ITransactionManager(zope.interface.Interface):
@@ -123,7 +119,7 @@
         This is called from the application.  This can only be called
         before the two-phase commit protocol has been started.
         """
-    
+
     def doom():
         """Doom the transaction.
 
@@ -231,7 +227,7 @@
         hooks.  Applications should take care to avoid creating infinite loops
         by recursively registering hooks.
 
-        Hooks are called only for a top-level commit.  A 
+        Hooks are called only for a top-level commit.  A
         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
@@ -252,7 +248,7 @@
 
     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
@@ -262,14 +258,14 @@
          (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 
+
+         Hooks are called only for a top-level commit.  A
          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
@@ -486,3 +482,9 @@
     """
 class DoomedTransaction(TransactionError):
     """A commit was attempted on a transaction that was doomed."""
+
+class TransientError(TransactionError):
+    """An error has occured when performing a transaction.
+
+    It's possible that retrying the transaction will succeed.
+    """

Added: transaction/trunk/transaction/tests/convenience.txt
===================================================================
--- transaction/trunk/transaction/tests/convenience.txt	                        (rev 0)
+++ transaction/trunk/transaction/tests/convenience.txt	2010-05-12 20:36:53 UTC (rev 112264)
@@ -0,0 +1,183 @@
+Transaction convenience support
+===============================
+
+(We *really* need to write proper documentation for the transaction
+ package, but I don't want to block the conveniences documented here
+ for that.)
+
+with support
+------------
+
+We can now use the with statement to define transaction boundaries.
+
+    >>> import transaction.tests.savepointsample
+    >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
+    >>> dm.keys()
+    []
+
+We can use the transaction module directly:
+
+    >>> with transaction as t:
+    ...     dm['z'] = 1
+    ...     t.note('test 1')
+
+    >>> dm['z']
+    1
+
+    >>> dm.last_note
+    'test 1'
+
+    >>> with transaction:
+    ...     dm['z'] = 2
+    ...     xxx
+    Traceback (most recent call last):
+    ...
+    NameError: name 'xxx' is not defined
+
+    >>> dm['z']
+    1
+
+We can use it with a manager:
+
+    >>> with transaction.manager as t:
+    ...     dm['z'] = 3
+    ...     t.note('test 3')
+
+    >>> dm['z']
+    3
+
+    >>> dm.last_note
+    'test 3'
+
+    >>> with transaction:
+    ...     dm['z'] = 4
+    ...     xxx
+    Traceback (most recent call last):
+    ...
+    NameError: name 'xxx' is not defined
+
+    >>> dm['z']
+    3
+
+Retries
+-------
+
+Commits can fail for transient reasons, especially conflicts.
+Applications will often retry transactions some number of times to
+overcome transient failures.  This typically looks something like::
+
+    for i in range(3):
+        try:
+           with transaction:
+               ... some something ...
+        except SomeTransientException:
+           contine
+        else:
+           break
+
+This is rather ugly.
+
+Transaction managers provide a helper for this case. To show this,
+we'll use a contrived example:
+
+
+    >>> ntry = 0
+    >>> with transaction:
+    ...      dm['ntry'] = 0
+
+    >>> import transaction.interfaces
+    >>> class Retry(transaction.interfaces.TransientError):
+    ...     pass
+
+    >>> for attempt in transaction.manager.attempts():
+    ...     with attempt as t:
+    ...         t.note('test')
+    ...         print dm['ntry'], ntry
+    ...         ntry += 1
+    ...         dm['ntry'] = ntry
+    ...         if ntry % 3:
+    ...             raise Retry(ntry)
+    0 0
+    0 1
+    0 2
+
+The raising of a subclass of TransientError is critical here. It's
+what signals that the transaction should be retried.  It is generally
+up to the data manager to signal that a transaction should try again
+by raising a subclass of TransientError (or TransientError itself, of
+course).
+
+You shouldn't make any assumptions about the object returned by the
+iterator.  (It isn't a transaction or transaction manager, as far as
+you know. :)  If you use the ``as`` keyword in the ``with`` statement,
+a transaction object will be assigned to the variable named.
+
+By default, it tries 3 times. We can tell it how many times to try:
+
+    >>> for attempt in transaction.manager.attempts(2):
+    ...     with attempt:
+    ...         ntry += 1
+    ...         if ntry % 3:
+    ...             raise Retry(ntry)
+    Traceback (most recent call last):
+    ...
+    Retry: 5
+
+It it doesn't succeed in that many times, the exception will be
+propagated.
+
+Of course, other errors are propagated directly:
+
+    >>> ntry = 0
+    >>> for attempt in transaction.manager.attempts():
+    ...     with attempt:
+    ...         ntry += 1
+    ...         if ntry == 3:
+    ...             raise ValueError(ntry)
+    Traceback (most recent call last):
+    ...
+    ValueError: 3
+
+We can use the default transaction manager:
+
+    >>> for attempt in transaction.attempts():
+    ...     with attempt as t:
+    ...         t.note('test')
+    ...         print dm['ntry'], ntry
+    ...         ntry += 1
+    ...         dm['ntry'] = ntry
+    ...         if ntry % 3:
+    ...             raise Retry(ntry)
+    3 3
+    3 4
+    3 5
+
+Sometimes, a data manager doesn't raise exceptions directly, but
+wraps other other systems that raise exceptions outside of it's
+control.  Data  managers can provide a should_retry method that takes
+an exception instance and returns True if the transaction should be
+attempted again.
+
+    >>> class DM(transaction.tests.savepointsample.SampleSavepointDataManager):
+    ...     def should_retry(self, e):
+    ...         if 'should retry' in str(e):
+    ...             return True
+
+    >>> ntry = 0
+    >>> dm2 = DM()
+    >>> with transaction:
+    ...     dm2['ntry'] = 0
+    >>> for attempt in transaction.manager.attempts():
+    ...     with attempt:
+    ...         print dm['ntry'], ntry
+    ...         ntry += 1
+    ...         dm['ntry'] = ntry
+    ...         dm2['ntry'] = ntry
+    ...         if ntry % 3:
+    ...             raise ValueError('we really should retry this')
+    6 0
+    6 1
+    6 2
+
+    >>> dm2['ntry']
+    3


Property changes on: transaction/trunk/transaction/tests/convenience.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Modified: transaction/trunk/transaction/tests/savepointsample.py
===================================================================
--- transaction/trunk/transaction/tests/savepointsample.py	2010-05-12 18:24:18 UTC (rev 112263)
+++ transaction/trunk/transaction/tests/savepointsample.py	2010-05-12 20:36:53 UTC (rev 112264)
@@ -84,6 +84,7 @@
             self.transaction.join(self)
 
     def _resetTransaction(self):
+        self.last_note = getattr(self.transaction, 'description', None)
         self.transaction = None
         self.tpc_phase = None
 

Modified: transaction/trunk/transaction/tests/test_transaction.py
===================================================================
--- transaction/trunk/transaction/tests/test_transaction.py	2010-05-12 18:24:18 UTC (rev 112263)
+++ transaction/trunk/transaction/tests/test_transaction.py	2010-05-12 20:36:53 UTC (rev 112264)
@@ -39,6 +39,7 @@
 from doctest import DocTestSuite, DocFileSuite
 
 import struct
+import sys
 import unittest
 import warnings
 
@@ -688,12 +689,16 @@
     """
 
 def test_suite():
-    return unittest.TestSuite((
+    suite = unittest.TestSuite((
         DocFileSuite('doom.txt'),
         DocTestSuite(),
         unittest.makeSuite(TransactionTests),
         ))
+    if sys.version_info >= (2, 6):
+        suite.addTest(DocFileSuite('convenience.txt'))
 
+    return suite
+
 # additional_tests is for setuptools "setup.py test" support
 additional_tests = test_suite
 



More information about the checkins mailing list