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()
>>> list(dm.keys())
[]

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.manager: #doctest ELLIPSIS
...     dm['z'] = 4
...     xxx
Traceback (most recent call last):
...
NameError: ... name 'xxx' is not defined

>>> dm['z']
3

On Python 2, you can also abbreviate with transaction.manager: as with transaction:. This does not work on Python 3 (see see http://bugs.python.org/issue12022).

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.manager:
           ... 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.manager:
...      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("%s %s" % (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("%s %s" % (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.manager:
...     dm2['ntry'] = 0
>>> for attempt in transaction.manager.attempts():
...     with attempt:
...         print("%s %s" % (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

Table Of Contents

Previous topic

transaction Documentation

Next topic

Dooming Transactions

This Page