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(u'test 3')

>>> dm['z']
3

>>> dm.last_note == u'test 3'
True

>>> 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:
       continue
    else:
       break

This is rather ugly and easy to get wrong.

Transaction managers provide two helpers for this case.

Running and retrying functions as transactions

The first helper runs a function as a transaction:

def do_somthing():
    "Do something"
    ... some something ...

transaction.manager.run(do_somthing)

You can also use this as a decorator, which executes the decorated function immediately [1]:

@transaction.manager.run
def _():
    "Do something"
    ... some something ...

The transaction manager run method will run the function and return the results. If the function raises a TransientError, the function will be retried a configurable number of times, 3 by default. Any other exceptions will be raised.

The function name (if it isn’t '_') and docstring, if any, are added to the transaction description.

You can pass an integer number of times to try to the run method:

transaction.manager.run(do_somthing, 9)

@transaction.manager.run(9)
def _():
    "Do something"
    ... some something ...

The default number of times to try is 3.

Retrying code blocks using a attempt iterator

An older helper for running transactions uses an iterator of attempts:

for attempt in transaction.manager.attempts():
    with attempt as t:
        ... some something ...

This runs the code block until it runs without a transient error or until the number of attempts is exceeded. By default, it tries 3 times, but you can pass a number of attempts:

for attempt in transaction.manager.attempts(9):
    with attempt as t:
        ... some something ...
[1]Some people find this easier to read, even though the result isn’t a decorated function, but rather the result of calling it in a transaction. The function name _ is used here to emphasize that the function is essentially being used as an anonymous function.