Dooming Transactions

A doomed transaction behaves exactly the same way as an active transaction but raises an error on any attempt to commit it, thus forcing an abort.

Doom is useful in places where abort is unsafe and an exception cannot be raised. This occurs when the programmer wants the code following the doom to run but not commit. It is unsafe to abort in these circumstances as a following get() may implicitly open a new transaction.

Any attempt to commit a doomed transaction will raise a DoomedTransaction exception.

An example of such a use case can be found in zope/app/form/browser/editview.py. Here a form validation failure must doom the transaction as committing the transaction may have side-effects. However, the form code must continue to calculate a form containing the error messages to return.

For Zope in general, code running within a request should always doom transactions rather than aborting them. It is the responsibilty of the publication to either abort() or commit() the transaction. Application code can use savepoints and doom() safely.

To see how it works we first need to create a stub data manager:

>>> from transaction.interfaces import IDataManager
>>> from zope.interface import implementer
>>> @implementer(IDataManager)
... class DataManager:
...     def __init__(self):
...         self.attr_counter = {}
...     def __getattr__(self, name):
...         def f(transaction):
...             self.attr_counter[name] = self.attr_counter.get(name, 0) + 1
...         return f
...     def total(self):
...         count = 0
...         for access_count in self.attr_counter.values():
...             count += access_count
...         return count
...     def sortKey(self):
...         return '1'

Start a new transaction:

>>> import transaction
>>> txn = transaction.begin()
>>> dm = DataManager()
>>> txn.join(dm)

We can ask a transaction if it is doomed to avoid expensive operations. An example of a use case is an object-relational mapper where a pre-commit hook sends all outstanding SQL to a relational database for objects changed during the transaction. This expensive operation is not necessary if the transaction has been doomed. A non-doomed transaction should return False:

>>> txn.isDoomed()
False

We can doom a transaction by calling .doom() on it:

>>> txn.doom()
>>> txn.isDoomed()
True

We can doom it again if we like:

>>> txn.doom()

The data manager is unchanged at this point:

>>> dm.total()
0

Attempting to commit a doomed transaction any number of times raises a DoomedTransaction:

>>> txn.commit() 
Traceback (most recent call last):
DoomedTransaction: transaction doomed, cannot commit
>>> txn.commit() 
Traceback (most recent call last):
DoomedTransaction: transaction doomed, cannot commit

But still leaves the data manager unchanged:

>>> dm.total()
0

But the doomed transaction can be aborted:

>>> txn.abort()

Which aborts the data manager:

>>> dm.total()
1
>>> dm.attr_counter['abort']
1

Dooming the current transaction can also be done directly from the transaction module. We can also begin a new transaction directly after dooming the old one:

>>> txn = transaction.begin()
>>> transaction.isDoomed()
False
>>> transaction.doom()
>>> transaction.isDoomed()
True
>>> txn = transaction.begin()

After committing a transaction we get an assertion error if we try to doom the transaction. This could be made more specific, but trying to doom a transaction after it’s been committed is probably a programming error:

>>> txn = transaction.begin()
>>> txn.commit()
>>> txn.doom()
Traceback (most recent call last):
    ...
ValueError: non-doomable

A doomed transaction should act the same as an active transaction, so we should be able to join it:

>>> txn = transaction.begin()
>>> txn.doom()
>>> dm2 = DataManager()
>>> txn.join(dm2)

Clean up:

>>> txn = transaction.begin()
>>> txn.abort()