Commit 9a41acdd authored by Jim Fulton's avatar Jim Fulton

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.
parent 8c8ce7ed
......@@ -2,9 +2,32 @@ Changes
=======
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.
......
......@@ -21,10 +21,12 @@ from transaction._manager import TransactionManager
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
......@@ -17,11 +17,14 @@ It coordinates application code and resource managers, so that they
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
......@@ -55,6 +58,7 @@ def _new_transaction(txn, synchs):
# so that Transactions "see" synchronizers that get registered after the
# Transaction object is constructed.
class TransactionManager(object):
def __init__(self):
......@@ -68,6 +72,8 @@ class TransactionManager(object):
_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 @@ class TransactionManager(object):
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 @@ class ThreadTransactionManager(TransactionManager):
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
......@@ -11,10 +11,6 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Transaction Interfaces
$Id$
"""
import zope.interface
......@@ -123,7 +119,7 @@ class ITransaction(zope.interface.Interface):
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 @@ class ITransaction(zope.interface.Interface):
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 @@ class ITransaction(zope.interface.Interface):
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 @@ class ITransaction(zope.interface.Interface):
(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 TransactionFailedError(TransactionError):
"""
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.
"""
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
......@@ -84,6 +84,7 @@ class SampleDataManager(UserDict.DictMixin):
self.transaction.join(self)
def _resetTransaction(self):
self.last_note = getattr(self.transaction, 'description', None)
self.transaction = None
self.tpc_phase = None
......
......@@ -39,6 +39,7 @@ TODO
from doctest import DocTestSuite, DocFileSuite
import struct
import sys
import unittest
import warnings
......@@ -688,11 +689,15 @@ def test_addAfterCommitHook():
"""
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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment