pyramid_tm
==========

Overview
--------

``pyramid_tm`` is a package which allows :term:`Pyramid` requests to join
the active :term:`transaction` as provided by the :term:`transaction` package.

Installation
------------

Install using setuptools, e.g. (within a virtualenv)::

  $ easy_install pyramid_tm

Setup
-----

Once ``pyramid_tm`` is installed, you must use the ``config.include``
mechanism to include it into your Pyramid project's configuration.  In your
Pyramid project's ``__init__.py``:

.. code-block:: python
   :linenos:

   config = Configurator(.....)
   config.include('pyramid_tm')

Or use the ``pyramid.includes`` configuration setting in your ``.ini`` file:

.. code-block:: ini
   :linenos:

   [app:myapp]
   pyramid.includes = pyramid_tm

After the package is included, whenever a new request enters the application,
a new transaction is associated with that request.

.. note::

   When the ``repoze.tm`` or ``repoze.tm2`` middleware is in the WSGI
   pipeline, ``pyramid_tm`` becomes inactive.

:term:`transaction` Usage
-------------------------

At the beginning of a request a new :term:`transaction` is started
using the ``transaction.begin()`` function.  Once the request has
finished all of it's works (ie views have finished running), a few checks
are tested:

  1) Did some a transaction.doom() cause the transaction to become "doomed"?
     if so, ``transaction.abort()``.

  2) Did an exception occur in the underlying code? if so,
     ``transaction.abort()``

  3) If the ``tm.commit_veto`` configuration setting was used, did
     the commit veto callback, called with the response generated by the
     application, return a result that evaluates to ``True``? if so,
     ``transaction.abort()``.

If none of these checks calls ``transaction.abort()`` then the transaction is
instead committed using ``transaction.commit()``.

By itself, this :term:`transaction` machinery doesn't do much.  It is up to
third-party code to *join* the active transaction to benefit.  See
`repoze.filesafe <http://pypi.python.org/pypi/repoze.filesafe>`_ for an
example of how files creation can be committed or rolled back based on
:term:`transaction` and the `pyramid_mailer
<http://docs.pylonsproject.org/projects/pyramid_mailer/dev/>`_ package to see
how you can prevent emails from being sent until a transaction succeeeds.
ZODB database connections are automatically joined to the transaction, as
well as SQLAlchemy connections which are configured with the
``ZopeTransactionExtension`` extension.

Adding a Commit Veto Hook
-------------------------

It is possible to configure ``pyramid_tm`` with a "commit veto" hook.  The
commit veto hook receives the request and the response.  It can examine both
of them, and return ``True`` if the transaction should be vetoed.  If the
transaction is vetoed, it will be aborted instead of committed.  By default,
``pyramid_tm`` does not configure a commit veto into the system; you must do
it explicitly.

:mod:`pyramid_tm` contains a :func:`pyramid_tm.default_commit_veto` that is
suitable for use when you want to abort when the response's status code
indicates non-success or if you'd like to signal that the transaction should
be aborted or committed using a response header.  The default commit veto
vetoes a commit if the status code starts with ``4`` or ``5`` or there is a
``X-Tm`` response header with a value that does not equal ``commit``.

.. code-block:: python
   :linenos:

   def default_commit_veto(request, response):
       xtm = response.headers.get('x-tm')
       if xtm is not None:
           if xtm == 'commit':
               return False
           return True
       status = response.status
       for bad in ('4', '5'):
           if status.startswith(bad):
               return True
       return False

If you'd like to use this commit veto in your system, you can do it via
Python:

.. code-block:: python
   :linenos:

   from pyramid.config import Configurator

   def app(global_conf, settings):
       settings['tm.commit_veto'] = 'pyramid_tm.default_commit_veto'
       config = Configurator(settings=settings)
       config.include('pyramid_tm')

Or via PasteDeploy:

.. code-block:: ini
   :linenos:

   [app:myapp]
   tm.commit_veto = pyramid_tm.default_commit_veto

If you'd like to use a different "commit veto" callback, create a function
with the same signature (``request``, ``response``) and return value
(``True`` or ``False``), then pass a ``tm.commit_veto`` key/value
pair in your settings which points at the Python dotted name of this commit
veto.

Via Python:

.. code-block:: python
   :linenos:

   from pyramid.config import Configurator

   def app(global_conf, settings):
       settings['tm.commit_veto'] = 'my.package.commit_veto'
       config = Configurator(settings=settings)
       config.include('pyramid_tm')

Via PasteDeploy:

.. code-block:: ini
   :linenos:

   [app:myapp]
   tm.commit_veto = my.package.commit_veto

In the PasteDeploy example, the path is a Python dotted name, where the dots
separate module and package names, and the colon separates a module from its
contents.  In the above example, the code would be implemented as a
"commit_veto" function which lives in the "package" submodule of the "my"
package.

Retrying
--------

When the transaction manager calls the downstream handler, if the handler
raises a "retryable" exception, the transaction manager can be configured to
attempt to call the downstream handler again with the same request, in effect
"replaying" the request.

By default, retrying is turned off.  To turn it on, use the
``tm.attempts`` configuration setting.  By default this setting is
``1``, meaning only one attempt will be tried, and no retry will happen even
if a retryable error is raised by the handler.  But if the value, for
example, is set to ``3``, the following set of events might happen.

- The first attempt to call the handler raises a retryable exception;
  a second attempt will be tried.

- The second attempt raises a retryable exception, the transaction manager
  will try the request again one more time.

- The third attempt also raises a retryable exception, at this point all
  attempts are used up and the "retryable" exception will be raised to its
  caller.

Or this might happen:

- The first attempt to call the handler raises a retryable exception;
  a second attempt will be tried.

- The second attempt returns a response without raising any exception.

- The response is returned to the caller.

Retryable exceptions include ```ZODB.POSException.ConflictError``, and
certain exceptions raised by various data managers, such as
``psycopg2.extensions.TransactionRollbackError``, ``cx_Oracle.DatabaseError``
where the exception's code is 8877.  Any exception which inherits from
``transaction.interfaces.TransientError`` will be treated with retry
behavior.

Explicit Tween Configuration
----------------------------

Note that the transaction manager is a Pyramid "tween", and it can be used in
the explicit tween list if its implicit position in the tween chain is
incorrect (see the output of ``paster ptweens``)::

   [app:myapp]
   pyramid.tweens = someothertween
                    pyramid.tweens.excview_tween_factory
                    pyramid_tm.tm_tween_factory

It usually belongs directly above the "MAIN" entry in the ``paster ptweens``
output, and will attempt to sort there by default as the result of having
``include('pyramid_tm')`` invoked.

More Information
----------------

.. toctree::
   :maxdepth: 1

   api.rst
   glossary.rst


Reporting Bugs / Development Versions
-------------------------------------

Visit http://github.com/Pylons/pyramid_tm to download development or
tagged versions.

Visit http://github.com/Pylons/pyramid_tm/issues to report bugs.

Indices and tables
------------------

* :ref:`glossary`
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
