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
