[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