Commit 3805b636 authored by Tres Seaver's avatar Tres Seaver

Merge 'sphinx' branch.

parents 7165a6f9 c0ba11cc
......@@ -4,6 +4,20 @@ Changes
1.3.1 (unreleased)
------------------
- Refactored existing doctests as Sphinx documentation (snippets are exercised
via 'tox').
- 100% unit test coverage.
- Raise ValueError from ``Transaction.doom`` if the transaction is in a
non-doomable state (rather than using ``assert``).
- Raise ValueError from ``TransactionManager.attempts`` if passed a
non-positive value (rather than using ``assert``).
- Raise ValueError from ``TransactionManager.free`` if passed a foreign
transaction (rather tna using ``assert``).
- Declared support for Python 3.3 in ``setup.py``, and added ``tox`` testing.
- When a non-retryable exception was raised as the result of a call to
......
......@@ -10,6 +10,8 @@ with support
We can now use the with statement to define transaction boundaries.
.. doctest::
>>> import transaction.tests.savepointsample
>>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
>>> list(dm.keys())
......@@ -17,6 +19,8 @@ We can now use the with statement to define transaction boundaries.
We can use it with a manager:
.. doctest::
>>> with transaction.manager as t:
... dm['z'] = 3
... t.note('test 3')
......@@ -46,7 +50,9 @@ 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::
overcome transient failures. This typically looks something like:
.. doctest::
for i in range(3):
try:
......@@ -62,6 +68,7 @@ This is rather ugly.
Transaction managers provide a helper for this case. To show this,
we'll use a contrived example:
.. doctest::
>>> ntry = 0
>>> with transaction.manager:
......@@ -96,6 +103,8 @@ 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:
.. doctest::
>>> for attempt in transaction.manager.attempts(2):
... with attempt:
... ntry += 1
......@@ -110,6 +119,8 @@ propagated.
Of course, other errors are propagated directly:
.. doctest::
>>> ntry = 0
>>> for attempt in transaction.manager.attempts():
... with attempt:
......@@ -122,6 +133,8 @@ Of course, other errors are propagated directly:
We can use the default transaction manager:
.. doctest::
>>> for attempt in transaction.attempts():
... with attempt as t:
... t.note('test')
......@@ -140,6 +153,8 @@ control. Data managers can provide a should_retry method that takes
an exception instance and returns True if the transaction should be
attempted again.
.. doctest::
>>> class DM(transaction.tests.savepointsample.SampleSavepointDataManager):
... def should_retry(self, e):
... if 'should retry' in str(e):
......
Writing a Data Manager
======================
Simple Data Manager
------------------
.. doctest::
>>> from transaction.tests.examples import DataManager
This :class:`transaction.tests.examples.DataManager` class
provides a trivial data-manager implementation and docstrings to illustrate
the the protocol and to provide a tool for writing tests.
Our sample data manager has state that is updated through an inc
method and through transaction operations.
When we create a sample data manager:
.. doctest::
>>> dm = DataManager()
It has two bits of state, state:
.. doctest::
>>> dm.state
0
and delta:
.. doctest::
>>> dm.delta
0
Both of which are initialized to 0. state is meant to model
committed state, while delta represents tentative changes within a
transaction. We change the state by calling inc:
.. doctest::
>>> dm.inc()
which updates delta:
.. doctest::
>>> dm.delta
1
but state isn't changed until we commit the transaction:
.. doctest::
>>> dm.state
0
To commit the changes, we use 2-phase commit. We execute the first
stage by calling prepare. We need to pass a transation. Our
sample data managers don't really use the transactions for much,
so we'll be lazy and use strings for transactions:
.. doctest::
>>> t1 = '1'
>>> dm.prepare(t1)
The sample data manager updates the state when we call prepare:
.. doctest::
>>> dm.state
1
>>> dm.delta
1
This is mainly so we can detect some affect of calling the methods.
Now if we call commit:
.. doctest::
>>> dm.commit(t1)
Our changes are"permanent". The state reflects the changes and the
delta has been reset to 0.
.. doctest::
>>> dm.state
1
>>> dm.delta
0
The :meth:`prepare` Method
----------------------------
Prepare to commit data
.. doctest::
>>> dm = DataManager()
>>> dm.inc()
>>> t1 = '1'
>>> dm.prepare(t1)
>>> dm.commit(t1)
>>> dm.state
1
>>> dm.inc()
>>> t2 = '2'
>>> dm.prepare(t2)
>>> dm.abort(t2)
>>> dm.state
1
It is en error to call prepare more than once without an intervening
commit or abort:
.. doctest::
>>> dm.prepare(t1)
>>> dm.prepare(t1)
Traceback (most recent call last):
...
TypeError: Already prepared
>>> dm.prepare(t2)
Traceback (most recent call last):
...
TypeError: Already prepared
>>> dm.abort(t1)
If there was a preceeding savepoint, the transaction must match:
.. doctest::
>>> rollback = dm.savepoint(t1)
>>> dm.prepare(t2)
Traceback (most recent call last):
,,,
TypeError: ('Transaction missmatch', '2', '1')
>>> dm.prepare(t1)
The :meth:`abort` method
--------------------------
The abort method can be called before two-phase commit to
throw away work done in the transaction:
.. doctest::
>>> dm = DataManager()
>>> dm.inc()
>>> dm.state, dm.delta
(0, 1)
>>> t1 = '1'
>>> dm.abort(t1)
>>> dm.state, dm.delta
(0, 0)
The abort method also throws away work done in savepoints:
.. doctest::
>>> dm.inc()
>>> r = dm.savepoint(t1)
>>> dm.inc()
>>> r = dm.savepoint(t1)
>>> dm.state, dm.delta
(0, 2)
>>> dm.abort(t1)
>>> dm.state, dm.delta
(0, 0)
If savepoints are used, abort must be passed the same
transaction:
.. doctest::
>>> dm.inc()
>>> r = dm.savepoint(t1)
>>> t2 = '2'
>>> dm.abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> dm.abort(t1)
The abort method is also used to abort a two-phase commit:
.. doctest::
>>> dm.inc()
>>> dm.state, dm.delta
(0, 1)
>>> dm.prepare(t1)
>>> dm.state, dm.delta
(1, 1)
>>> dm.abort(t1)
>>> dm.state, dm.delta
(0, 0)
Of course, the transactions passed to prepare and abort must
match:
.. doctest::
>>> dm.prepare(t1)
>>> dm.abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> dm.abort(t1)
The :meth:`commit` method
---------------------------
Called to omplete two-phase commit
.. doctest::
>>> dm = DataManager()
>>> dm.state
0
>>> dm.inc()
We start two-phase commit by calling prepare:
.. doctest::
>>> t1 = '1'
>>> dm.prepare(t1)
We complete it by calling commit:
.. doctest::
>>> dm.commit(t1)
>>> dm.state
1
It is an error ro call commit without calling prepare first:
.. doctest::
>>> dm.inc()
>>> t2 = '2'
>>> dm.commit(t2)
Traceback (most recent call last):
...
TypeError: Not prepared to commit
>>> dm.prepare(t2)
>>> dm.commit(t2)
If course, the transactions given to prepare and commit must
be the same:
.. doctest::
>>> dm.inc()
>>> t3 = '3'
>>> dm.prepare(t3)
>>> dm.commit(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '3')
The :meth:`savepoint` method
------------------------------
Provide the ability to rollback transaction state
Savepoints provide a way to:
- Save partial transaction work. For some data managers, this
could allow resources to be used more efficiently.
- Provide the ability to revert state to a point in a
transaction without aborting the entire transaction. In
other words, savepoints support partial aborts.
Savepoints don't use two-phase commit. If there are errors in
setting or rolling back to savepoints, the application should
abort the containing transaction. This is *not* the
responsibility of the data manager.
Savepoints are always associated with a transaction. Any work
done in a savepoint's transaction is tentative until the
transaction is committed using two-phase commit.
.. doctest::
>>> dm = DataManager()
>>> dm.inc()
>>> t1 = '1'
>>> r = dm.savepoint(t1)
>>> dm.state, dm.delta
(0, 1)
>>> dm.inc()
>>> dm.state, dm.delta
(0, 2)
>>> r.rollback()
>>> dm.state, dm.delta
(0, 1)
>>> dm.prepare(t1)
>>> dm.commit(t1)
>>> dm.state, dm.delta
(1, 0)
Savepoints must have the same transaction:
.. doctest::
>>> r1 = dm.savepoint(t1)
>>> dm.state, dm.delta
(1, 0)
>>> dm.inc()
>>> dm.state, dm.delta
(1, 1)
>>> t2 = '2'
>>> r2 = dm.savepoint(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> r2 = dm.savepoint(t1)
>>> dm.inc()
>>> dm.state, dm.delta
(1, 2)
If we rollback to an earlier savepoint, we discard all work
done later:
.. doctest::
>>> r1.rollback()
>>> dm.state, dm.delta
(1, 0)
and we can no longer rollback to the later savepoint:
.. doctest::
>>> r2.rollback()
Traceback (most recent call last):
...
TypeError: ('Attempt to roll back to invalid save point', 3, 2)
We can roll back to a savepoint as often as we like:
.. doctest::
>>> r1.rollback()
>>> r1.rollback()
>>> r1.rollback()
>>> dm.state, dm.delta
(1, 0)
>>> dm.inc()
>>> dm.inc()
>>> dm.inc()
>>> dm.state, dm.delta
(1, 3)
>>> r1.rollback()
>>> dm.state, dm.delta
(1, 0)
But we can't rollback to a savepoint after it has been
committed:
.. doctest::
>>> dm.prepare(t1)
>>> dm.commit(t1)
>>> r1.rollback()
Traceback (most recent call last):
...
TypeError: Attempt to rollback stale rollback
......@@ -25,6 +25,8 @@ use savepoints and doom() safely.
To see how it works we first need to create a stub data manager:
.. doctest::
>>> from transaction.interfaces import IDataManager
>>> from zope.interface import implementer
>>> @implementer(IDataManager)
......@@ -45,6 +47,8 @@ To see how it works we first need to create a stub data manager:
Start a new transaction:
.. doctest::
>>> import transaction
>>> txn = transaction.begin()
>>> dm = DataManager()
......@@ -56,27 +60,37 @@ 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:
.. doctest::
>>> txn.isDoomed()
False
We can doom a transaction by calling .doom() on it:
.. doctest::
>>> txn.doom()
>>> txn.isDoomed()
True
We can doom it again if we like:
.. doctest::
>>> txn.doom()
The data manager is unchanged at this point:
.. doctest::
>>> dm.total()
0
Attempting to commit a doomed transaction any number of times raises a
DoomedTransaction:
.. doctest::
>>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
DoomedTransaction: transaction doomed, cannot commit
......@@ -86,15 +100,21 @@ DoomedTransaction:
But still leaves the data manager unchanged:
.. doctest::
>>> dm.total()
0
But the doomed transaction can be aborted:
.. doctest::
>>> txn.abort()
Which aborts the data manager:
.. doctest::
>>> dm.total()
1
>>> dm.attr_counter['abort']
......@@ -103,6 +123,8 @@ Which aborts the data manager:
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:
.. doctest::
>>> txn = transaction.begin()
>>> transaction.isDoomed()
False
......@@ -115,16 +137,20 @@ 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:
.. doctest::
>>> txn = transaction.begin()
>>> txn.commit()
>>> txn.doom()
Traceback (most recent call last):
...
AssertionError
ValueError: non-doomable
A doomed transaction should act the same as an active transaction, so we should
be able to join it:
.. doctest::
>>> txn = transaction.begin()
>>> txn.doom()
>>> dm2 = DataManager()
......@@ -132,5 +158,7 @@ be able to join it:
Clean up:
.. doctest::
>>> txn = transaction.begin()
>>> txn.abort()
Hooking the Transaction Machinery
=================================
The :meth:`addBeforeCommitHook` Method
--------------------------------------
Let's define a hook to call, and a way to see that it was called.
.. doctest::
>>> log = []
>>> def reset_log():
... del log[:]
>>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
... log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))
Now register the hook with a transaction.
.. doctest::
>>> from transaction import begin
>>> from transaction._compat import func_name
>>> import transaction
>>> t = begin()
>>> t.addBeforeCommitHook(hook, '1')
We can see that the hook is indeed registered.
.. doctest::
>>> [(func_name(hook), args, kws)
... for hook, args, kws in t.getBeforeCommitHooks()]
[('hook', ('1',), {})]
When transaction commit starts, the hook is called, with its
arguments.
.. doctest::
>>> log
[]
>>> t.commit()
>>> log
["arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
A hook's registration is consumed whenever the hook is called. Since
the hook above was called, it's no longer registered:
.. doctest::
>>> from transaction import commit
>>> len(list(t.getBeforeCommitHooks()))
0
>>> commit()
>>> log
[]
The hook is only called for a full commit, not for a savepoint.
.. doctest::
>>> t = begin()
>>> t.addBeforeCommitHook(hook, 'A', dict(kw1='B'))
>>> dummy = t.savepoint()
>>> log
[]
>>> t.commit()
>>> log
["arg 'A' kw1 'B' kw2 'no_kw2'"]
>>> reset_log()
If a transaction is aborted, no hook is called.
.. doctest::
>>> from transaction import abort
>>> t = begin()
>>> t.addBeforeCommitHook(hook, ["OOPS!"])
>>> abort()
>>> log
[]
>>> commit()
>>> log
[]
The hook is called before the commit does anything, so even if the
commit fails the hook will have been called. To provoke failures in
commit, we'll add failing resource manager to the transaction.
.. doctest::
>>> class CommitFailure(Exception):
... pass
>>> class FailingDataManager:
... def tpc_begin(self, txn, sub=False):
... raise CommitFailure('failed')
... def abort(self, txn):
... pass
>>> t = begin()
>>> t.join(FailingDataManager())
>>> t.addBeforeCommitHook(hook, '2')
>>> from transaction.tests.common import DummyFile
>>> from transaction.tests.common import Monkey
>>> from transaction.tests.common import assertRaisesEx
>>> from transaction import _transaction
>>> buffer = DummyFile()
>>> with Monkey(_transaction, _TB_BUFFER=buffer):
... err = assertRaisesEx(CommitFailure, t.commit)
>>> log
["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
Let's register several hooks.
.. doctest::
>>> t = begin()
>>> t.addBeforeCommitHook(hook, '4', dict(kw1='4.1'))
>>> t.addBeforeCommitHook(hook, '5', dict(kw2='5.2'))
They are returned in the same order by getBeforeCommitHooks.
.. doctest::
>>> [(func_name(hook), args, kws) #doctest: +NORMALIZE_WHITESPACE
... for hook, args, kws in t.getBeforeCommitHooks()]
[('hook', ('4',), {'kw1': '4.1'}),
('hook', ('5',), {'kw2': '5.2'})]
And commit also calls them in this order.
.. doctest::
>>> t.commit()
>>> len(log)
2
>>> log #doctest: +NORMALIZE_WHITESPACE
["arg '4' kw1 '4.1' kw2 'no_kw2'",
"arg '5' kw1 'no_kw1' kw2 '5.2'"]
>>> reset_log()
While executing, a hook can itself add more hooks, and they will all
be called before the real commit starts.
.. doctest::
>>> def recurse(txn, arg):
... log.append('rec' + str(arg))
... if arg:
... txn.addBeforeCommitHook(hook, '-')
... txn.addBeforeCommitHook(recurse, (txn, arg-1))
>>> t = begin()
>>> t.addBeforeCommitHook(recurse, (t, 3))
>>> commit()
>>> log #doctest: +NORMALIZE_WHITESPACE
['rec3',
"arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec2',
"arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec1',
"arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec0']
>>> reset_log()
The :meth:`addAfterCommitHook` Method
--------------------------------------
Let's define a hook to call, and a way to see that it was called.
.. doctest::
>>> log = []
>>> def reset_log():
... del log[:]
>>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
... log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2))
Now register the hook with a transaction.
.. doctest::
>>> from transaction import begin
>>> from transaction._compat import func_name
>>> t = begin()
>>> t.addAfterCommitHook(hook, '1')
We can see that the hook is indeed registered.
.. doctest::
>>> [(func_name(hook), args, kws)
... for hook, args, kws in t.getAfterCommitHooks()]
[('hook', ('1',), {})]
When transaction commit is done, the hook is called, with its
arguments.
.. doctest::
>>> log
[]
>>> t.commit()
>>> log
["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
A hook's registration is consumed whenever the hook is called. Since
the hook above was called, it's no longer registered:
.. doctest::
>>> from transaction import commit
>>> len(list(t.getAfterCommitHooks()))
0
>>> commit()
>>> log
[]
The hook is only called after a full commit, not for a savepoint.
.. doctest::
>>> t = begin()
>>> t.addAfterCommitHook(hook, 'A', dict(kw1='B'))
>>> dummy = t.savepoint()
>>> log
[]
>>> t.commit()
>>> log
["True arg 'A' kw1 'B' kw2 'no_kw2'"]
>>> reset_log()
If a transaction is aborted, no hook is called.
.. doctest::
>>> from transaction import abort
>>> t = begin()
>>> t.addAfterCommitHook(hook, ["OOPS!"])
>>> abort()
>>> log
[]
>>> commit()
>>> log
[]
The hook is called after the commit is done, so even if the
commit fails the hook will have been called. To provoke failures in
commit, we'll add failing resource manager to the transaction.
.. doctest::
>>> class CommitFailure(Exception):
... pass
>>> class FailingDataManager:
... def tpc_begin(self, txn):
... raise CommitFailure('failed')
... def abort(self, txn):
... pass
>>> t = begin()
>>> t.join(FailingDataManager())
>>> t.addAfterCommitHook(hook, '2')
>>> from transaction.tests.common import DummyFile
>>> from transaction.tests.common import Monkey
>>> from transaction.tests.common import assertRaisesEx
>>> from transaction import _transaction
>>> buffer = DummyFile()
>>> with Monkey(_transaction, _TB_BUFFER=buffer):
... err = assertRaisesEx(CommitFailure, t.commit)
>>> log
["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
Let's register several hooks.
.. doctest::
>>> t = begin()
>>> t.addAfterCommitHook(hook, '4', dict(kw1='4.1'))
>>> t.addAfterCommitHook(hook, '5', dict(kw2='5.2'))
They are returned in the same order by getAfterCommitHooks.
.. doctest::
>>> [(func_name(hook), args, kws) #doctest: +NORMALIZE_WHITESPACE
... for hook, args, kws in t.getAfterCommitHooks()]
[('hook', ('4',), {'kw1': '4.1'}),
('hook', ('5',), {'kw2': '5.2'})]
And commit also calls them in this order.
.. doctest::
>>> t.commit()
>>> len(log)
2
>>> log #doctest: +NORMALIZE_WHITESPACE
["True arg '4' kw1 '4.1' kw2 'no_kw2'",
"True arg '5' kw1 'no_kw1' kw2 '5.2'"]
>>> reset_log()
While executing, a hook can itself add more hooks, and they will all
be called before the real commit starts.
.. doctest::
>>> def recurse(status, txn, arg):
... log.append('rec' + str(arg))
... if arg:
... txn.addAfterCommitHook(hook, '-')
... txn.addAfterCommitHook(recurse, (txn, arg-1))
>>> t = begin()
>>> t.addAfterCommitHook(recurse, (t, 3))
>>> commit()
>>> log #doctest: +NORMALIZE_WHITESPACE
['rec3',
"True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec2',
"True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec1',
"True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec0']
>>> reset_log()
If an after commit hook is raising an exception then it will log a
message at error level so that if other hooks are registered they
can be executed. We don't support execution dependencies at this level.
.. doctest::
>>> from transaction import TransactionManager
>>> from transaction.tests.test__manager import DataObject
>>> mgr = TransactionManager()
>>> do = DataObject(mgr)
>>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
... raise TypeError("Fake raise")
>>> t = begin()
>>> t.addAfterCommitHook(hook, ('-', 1))
>>> t.addAfterCommitHook(hookRaise, ('-', 2))
>>> t.addAfterCommitHook(hook, ('-', 3))
>>> commit()
>>> log
["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"]
>>> reset_log()
Test that the associated transaction manager has been cleanup when
after commit hooks are registered
.. doctest::
>>> mgr = TransactionManager()
>>> do = DataObject(mgr)
>>> t = begin()
>>> t._manager._txn is not None
True
>>> t.addAfterCommitHook(hook, ('-', 1))
>>> commit()
>>> log
["True arg '-' kw1 1 kw2 'no_kw2'"]
>>> t._manager._txn is not None
False
>>> reset_log()
:mod:`transaction` Documentation
================================
Transaction objects manage resources for an individual activity.
Compatibility issues
--------------------
The implementation of Transaction objects involves two layers of
backwards compatibility, because this version of transaction supports
both ZODB 3 and ZODB 4. Zope is evolving towards the ZODB4
interfaces.
Transaction has two methods for a resource manager to call to
participate in a transaction -- register() and join(). join() takes a
resource manager and adds it to the list of resources. register() is
for backwards compatibility. It takes a persistent object and
registers its _p_jar attribute. TODO: explain adapter
Two-phase commit
----------------
A transaction commit involves an interaction between the transaction
object and one or more resource managers. The transaction manager
calls the following four methods on each resource manager; it calls
tpc_begin() on each resource manager before calling commit() on any of
them.
1. tpc_begin(txn)
2. commit(txn)
3. tpc_vote(txn)
4. tpc_finish(txn)
Before-commit hook
------------------
Sometimes, applications want to execute some code when a transaction is
committed. For example, one might want to delay object indexing until a
transaction commits, rather than indexing every time an object is changed.
Or someone might want to check invariants only after a set of operations. A
pre-commit hook is available for such use cases: use addBeforeCommitHook(),
passing it a callable and arguments. The callable will be called with its
arguments at the start of the commit (but not for substransaction commits).
After-commit hook
------------------
Sometimes, applications want to execute code after a transaction commit
attempt succeeds or aborts. For example, one might want to launch non
transactional code after a successful commit. Or still someone might
want to launch asynchronous code after. A post-commit hook is
available for such use cases: use addAfterCommitHook(), passing it a
callable and arguments. The callable will be called with a Boolean
value representing the status of the commit operation as first
argument (true if successfull or false iff aborted) preceding its
arguments at the start of the commit (but not for substransaction
commits). Commit hooks are not called for transaction.abort().
Error handling
--------------
When errors occur during two-phase commit, the transaction manager
aborts all the resource managers. The specific methods it calls
depend on whether the error occurs before or after the call to
tpc_vote() on that transaction manager.
If the resource manager has not voted, then the resource manager will
have one or more uncommitted objects. There are two cases that lead
to this state; either the transaction manager has not called commit()
for any objects on this resource manager or the call that failed was a
commit() for one of the objects of this resource manager. For each
uncommitted object, including the object that failed in its commit(),
call abort().
Once uncommitted objects are aborted, tpc_abort() or abort_sub() is
called on each resource manager.
Synchronization
---------------
You can register sychronization objects (synchronizers) with the
tranasction manager. The synchronizer must implement
beforeCompletion() and afterCompletion() methods. The transaction
manager calls beforeCompletion() when it starts a top-level two-phase
commit. It calls afterCompletion() when a top-level transaction is
committed or aborted. The methods are passed the current Transaction
as their only argument.
Contents:
.. toctree::
:maxdepth: 2
convenience
doom
savepoint
hooks
datamanager
resourcemanager
api
......
Writing a Resource Manager
==========================
Simple Resource Manager
-----------------------
.. doctest::
>>> from transaction.tests.examples import ResourceManager
This :class:`transaction.tests.examples.ResourceManager`
class provides a trivial resource-manager implementation and doc
strings to illustrate the protocol and to provide a tool for writing
tests.
Our sample resource manager has state that is updated through an inc
method and through transaction operations.
When we create a sample resource manager:
.. doctest::
>>> rm = ResourceManager()
It has two pieces state, state and delta, both initialized to 0:
.. doctest::
>>> rm.state
0
>>> rm.delta
0
state is meant to model committed state, while delta represents
tentative changes within a transaction. We change the state by
calling inc:
.. doctest::
>>> rm.inc()
which updates delta:
.. doctest::
>>> rm.delta
1
but state isn't changed until we commit the transaction:
.. doctest::
>>> rm.state
0
To commit the changes, we use 2-phase commit. We execute the first
stage by calling prepare. We need to pass a transation. Our
sample resource managers don't really use the transactions for much,
so we'll be lazy and use strings for transactions. The sample
resource manager updates the state when we call tpc_vote:
.. doctest::
>>> t1 = '1'
>>> rm.tpc_begin(t1)
>>> rm.state, rm.delta
(0, 1)
>>> rm.tpc_vote(t1)
>>> rm.state, rm.delta
(1, 1)
Now if we call tpc_finish:
>>> rm.tpc_finish(t1)
Our changes are "permanent". The state reflects the changes and the
delta has been reset to 0.
.. doctest::
>>> rm.state, rm.delta
(1, 0)
The :meth:`tpc_begin` Method
-----------------------------
Called by the transaction manager to ask the RM to prepare to commit data.
.. doctest::
>>> rm = ResourceManager()
>>> rm.inc()
>>> t1 = '1'
>>> rm.tpc_begin(t1)
>>> rm.tpc_vote(t1)
>>> rm.tpc_finish(t1)
>>> rm.state
1
>>> rm.inc()
>>> t2 = '2'
>>> rm.tpc_begin(t2)
>>> rm.tpc_vote(t2)
>>> rm.tpc_abort(t2)
>>> rm.state
1
It is an error to call tpc_begin more than once without completing
two-phase commit:
.. doctest::
>>> rm.tpc_begin(t1)
>>> rm.tpc_begin(t1)
Traceback (most recent call last):
...
ValueError: txn in state 'tpc_begin' but expected one of (None,)
>>> rm.tpc_abort(t1)
If there was a preceeding savepoint, the transaction must match:
.. doctest::
>>> rollback = rm.savepoint(t1)
>>> rm.tpc_begin(t2)
Traceback (most recent call last):
,,,
TypeError: ('Transaction missmatch', '2', '1')
>>> rm.tpc_begin(t1)
The :meth:`tpc_vote` Method
---------------------------
Verify that a data manager can commit the transaction.
This is the last chance for a data manager to vote 'no'. A
data manager votes 'no' by raising an exception.
Passed `transaction`, which is the ITransaction instance associated with the
transaction being committed.
The :meth:`tpc_finish` Method
-----------------------------
Complete two-phase commit
.. doctest::
>>> rm = ResourceManager()
>>> rm.state
0
>>> rm.inc()
We start two-phase commit by calling prepare:
>>> t1 = '1'
>>> rm.tpc_begin(t1)
>>> rm.tpc_vote(t1)
We complete it by calling tpc_finish:
>>> rm.tpc_finish(t1)
>>> rm.state
1
It is an error ro call tpc_finish without calling tpc_vote:
.. doctest::
>>> rm.inc()
>>> t2 = '2'
>>> rm.tpc_begin(t2)
>>> rm.tpc_finish(t2)
Traceback (most recent call last):
...
ValueError: txn in state 'tpc_begin' but expected one of ('tpc_vote',)
>>> rm.tpc_abort(t2) # clean slate
>>> rm.tpc_begin(t2)
>>> rm.tpc_vote(t2)
>>> rm.tpc_finish(t2)
Of course, the transactions given to tpc_begin and tpc_finish must
be the same:
.. doctest::
>>> rm.inc()
>>> t3 = '3'
>>> rm.tpc_begin(t3)
>>> rm.tpc_vote(t3)
>>> rm.tpc_finish(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '3')
The :meth:`tpc_abort` Method
-----------------------------
Abort a transaction
The abort method can be called before two-phase commit to
throw away work done in the transaction:
.. doctest::
>>> rm = ResourceManager()
>>> rm.inc()
>>> rm.state, rm.delta
(0, 1)
>>> t1 = '1'
>>> rm.tpc_abort(t1)
>>> rm.state, rm.delta
(0, 0)
The abort method also throws away work done in savepoints:
.. doctest::
>>> rm.inc()
>>> r = rm.savepoint(t1)
>>> rm.inc()
>>> r = rm.savepoint(t1)
>>> rm.state, rm.delta
(0, 2)
>>> rm.tpc_abort(t1)
>>> rm.state, rm.delta
(0, 0)
If savepoints are used, abort must be passed the same
transaction:
.. doctest::
>>> rm.inc()
>>> r = rm.savepoint(t1)
>>> t2 = '2'
>>> rm.tpc_abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> rm.tpc_abort(t1)
The abort method is also used to abort a two-phase commit:
.. doctest::
>>> rm.inc()
>>> rm.state, rm.delta
(0, 1)
>>> rm.tpc_begin(t1)
>>> rm.state, rm.delta
(0, 1)
>>> rm.tpc_vote(t1)
>>> rm.state, rm.delta
(1, 1)
>>> rm.tpc_abort(t1)
>>> rm.state, rm.delta
(0, 0)
Of course, the transactions passed to prepare and abort must
match:
.. doctest::
>>> rm.tpc_begin(t1)
>>> rm.tpc_abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> rm.tpc_abort(t1)
This should never fail.
The :meth:`savepoint` Method
----------------------------
Provide the ability to rollback transaction state
Savepoints provide a way to:
- Save partial transaction work. For some resource managers, this
could allow resources to be used more efficiently.
- Provide the ability to revert state to a point in a
transaction without aborting the entire transaction. In
other words, savepoints support partial aborts.
Savepoints don't use two-phase commit. If there are errors in
setting or rolling back to savepoints, the application should
abort the containing transaction. This is *not* the
responsibility of the resource manager.
Savepoints are always associated with a transaction. Any work
done in a savepoint's transaction is tentative until the
transaction is committed using two-phase commit.
.. doctest::
>>> rm = ResourceManager()
>>> rm.inc()
>>> t1 = '1'
>>> r = rm.savepoint(t1)
>>> rm.state, rm.delta
(0, 1)
>>> rm.inc()
>>> rm.state, rm.delta
(0, 2)
>>> r.rollback()
>>> rm.state, rm.delta
(0, 1)
>>> rm.tpc_begin(t1)
>>> rm.tpc_vote(t1)
>>> rm.tpc_finish(t1)
>>> rm.state, rm.delta
(1, 0)
Savepoints must have the same transaction:
.. doctest::
>>> r1 = rm.savepoint(t1)
>>> rm.state, rm.delta
(1, 0)
>>> rm.inc()
>>> rm.state, rm.delta
(1, 1)
>>> t2 = '2'
>>> r2 = rm.savepoint(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> r2 = rm.savepoint(t1)
>>> rm.inc()
>>> rm.state, rm.delta
(1, 2)
If we rollback to an earlier savepoint, we discard all work
done later:
.. doctest::
>>> r1.rollback()
>>> rm.state, rm.delta
(1, 0)
and we can no longer rollback to the later savepoint:
.. doctest::
>>> r2.rollback()
Traceback (most recent call last):
...
TypeError: ('Attempt to roll back to invalid save point', 3, 2)
We can roll back to a savepoint as often as we like:
.. doctest::
>>> r1.rollback()
>>> r1.rollback()
>>> r1.rollback()
>>> rm.state, rm.delta
(1, 0)
>>> rm.inc()
>>> rm.inc()
>>> rm.inc()
>>> rm.state, rm.delta
(1, 3)
>>> r1.rollback()
>>> rm.state, rm.delta
(1, 0)
But we can't rollback to a savepoint after it has been
committed:
.. doctest::
>>> rm.tpc_begin(t1)
>>> rm.tpc_vote(t1)
>>> rm.tpc_finish(t1)
>>> r1.rollback()
Traceback (most recent call last):
...
TypeError: Attempt to rollback stale rollback
......@@ -24,6 +24,8 @@ demonstrating the correct operation of savepoint support within the
transaction system. This data manager is very simple. It provides flat
storage of named immutable values, like strings and numbers.
.. doctest::
>>> import transaction
>>> from transaction.tests import savepointsample
>>> dm = savepointsample.SampleSavepointDataManager()
......@@ -31,12 +33,16 @@ storage of named immutable values, like strings and numbers.
As with other data managers, we can commit changes:
.. doctest::
>>> transaction.commit()
>>> dm['name']
'bob'
and abort changes:
.. doctest::
>>> dm['name'] = 'sally'
>>> dm['name']
'sally'
......@@ -52,6 +58,8 @@ account is invalid, we roll back the change for that entry. The success or
failure of an entry is indicated in the output status. First we'll initialize
some accounts:
.. doctest::
>>> dm['bob-balance'] = 0.0
>>> dm['bob-credit'] = 0.0
>>> dm['sally-balance'] = 0.0
......@@ -60,6 +68,8 @@ some accounts:
Now, we'll define a validation function to validate an account:
.. doctest::
>>> def validate_account(name):
... if dm[name+'-balance'] + dm[name+'-credit'] < 0:
... raise ValueError('Overdrawn', name)
......@@ -67,6 +77,8 @@ Now, we'll define a validation function to validate an account:
And a function to apply entries. If the function fails in some unexpected
way, it rolls back all of its changes and prints the error:
.. doctest::
>>> def apply_entries(entries):
... savepoint = transaction.savepoint()
... try:
......@@ -86,6 +98,8 @@ way, it rolls back all of its changes and prints the error:
Now let's try applying some entries:
.. doctest::
>>> apply_entries([
... ('bob', 10.0),
... ('sally', 10.0),
......@@ -109,6 +123,8 @@ Now let's try applying some entries:
If we provide entries that cause an unexpected error:
.. doctest::
>>> apply_entries([
... ('bob', 10.0),
... ('sally', 10.0),
......@@ -123,6 +139,8 @@ Because the apply_entries used a savepoint for the entire function, it was
able to rollback the partial changes without rolling back changes made in the
previous call to ``apply_entries``:
.. doctest::
>>> dm['bob-balance']
30.0
......@@ -132,6 +150,8 @@ previous call to ``apply_entries``:
If we now abort the outer transactions, the earlier changes will go
away:
.. doctest::
>>> transaction.abort()
>>> dm['bob-balance']
......@@ -145,6 +165,8 @@ Savepoint invalidation
A savepoint can be used any number of times:
.. doctest::
>>> dm['bob-balance'] = 100.0
>>> dm['bob-balance']
100.0
......@@ -170,6 +192,8 @@ A savepoint can be used any number of times:
However, using a savepoint invalidates any savepoints that come after it:
.. doctest::
>>> dm['bob-balance'] = 200.0
>>> dm['bob-balance']
200.0
......@@ -203,6 +227,8 @@ Databases without savepoint support
Normally it's an error to use savepoints with databases that don't support
savepoints:
.. doctest::
>>> dm_no_sp = savepointsample.SampleDataManager()
>>> dm_no_sp['name'] = 'bob'
>>> transaction.commit()
......@@ -219,6 +245,8 @@ that databases without savepoint support should be tolerated until a savepoint
is rolled back. This allows transactions to proceed if there are no reasons
to roll back:
.. doctest::
>>> dm_no_sp['name'] = 'sally'
>>> savepoint = transaction.savepoint(1)
>>> dm_no_sp['name'] = 'sue'
......@@ -245,6 +273,8 @@ the transaction is aborted.
In the previous example, we got an error when we tried to rollback the
savepoint. If we try to commit the transaction, the commit will fail:
.. doctest::
>>> transaction.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
......@@ -255,11 +285,15 @@ savepoint. If we try to commit the transaction, the commit will fail:
We have to abort it to make any progress:
.. doctest::
>>> transaction.abort()
Similarly, in our earlier example, where we tried to take a savepoint with a
data manager that didn't support savepoints:
.. doctest::
>>> dm_no_sp['name'] = 'sally'
>>> dm['name'] = 'sally'
>>> savepoint = transaction.savepoint() # doctest: +IGNORE_EXCEPTION_DETAIL
......@@ -280,6 +314,8 @@ data manager that didn't support savepoints:
After clearing the transaction with an abort, we can get on with new
transactions:
.. doctest::
>>> dm_no_sp['name'] = 'sally'
>>> dm['name'] = 'sally'
>>> transaction.commit()
......
......@@ -60,7 +60,7 @@ setup(name='transaction',
'zope.interface',
],
extras_require = {
'docs': ['Sphinx'],
'docs': ['Sphinx', 'repoze.sphinx.autointerface'],
'testing': ['nose', 'coverage'],
},
entry_points = """\
......
......@@ -18,33 +18,28 @@ else:
binary_type = str
long = long
def text_(s, encoding='latin-1', errors='strict'):
if isinstance(s, binary_type):
return s.decode(encoding, errors)
return s # pragma: no cover
def bytes_(s, encoding='latin-1', errors='strict'):
def bytes_(s, encoding='latin-1', errors='strict'): #pragma NO COVER
if isinstance(s, text_type):
return s.encode(encoding, errors)
return s
if PY3: # pragma: no cover
def native_(s, encoding='latin-1', errors='strict'):
def native_(s, encoding='latin-1', errors='strict'): #pragma NO COVER
if isinstance(s, text_type):
return s
return str(s, encoding, errors)
else:
def native_(s, encoding='latin-1', errors='strict'):
def native_(s, encoding='latin-1', errors='strict'): #pragma NO COVER
if isinstance(s, text_type):
return s.encode(encoding, errors)
return str(s)
if PY3:
if PY3: #pragma NO COVER
from io import StringIO
else:
from io import BytesIO as StringIO
if PY3:
if PY3: #pragma NO COVER
from collections import MutableMapping
else:
from UserDict import UserDict as MutableMapping
......@@ -54,13 +49,13 @@ if PY3: # pragma: no cover
exec_ = getattr(builtins, "exec")
def reraise(tp, value, tb=None):
def reraise(tp, value, tb=None): #pragma NO COVER
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value
else: # pragma: no cover
def exec_(code, globs=None, locs=None):
def exec_(code, globs=None, locs=None): #pragma NO COVER
"""Execute code in a namespace."""
if globs is None:
frame = sys._getframe(1)
......@@ -77,7 +72,7 @@ else: # pragma: no cover
""")
if PY3:
if PY3: #pragma NO COVER
try:
from threading import get_ident as get_thread_ident
except ImportError:
......@@ -87,9 +82,9 @@ else:
if PY3:
def func_name(func):
def func_name(func): #pragma NO COVER
return func.__name__
else:
def func_name(func):
def func_name(func): #pragma NO COVER
return func.func_name
......@@ -21,11 +21,11 @@ import threading
from zope.interface import implementer
from transaction.weakset import WeakSet
from transaction._transaction import Transaction
from transaction.interfaces import ITransactionManager
from transaction.interfaces import TransientError
from transaction.compat import reraise
from transaction.weakset import WeakSet
from transaction._compat import reraise
from transaction._transaction import Transaction
# We have to remember sets of synch objects, especially Connections.
......@@ -54,6 +54,7 @@ def _new_transaction(txn, synchs):
# so that Transactions "see" synchronizers that get registered after the
# Transaction object is constructed.
@implementer(ITransactionManager)
class TransactionManager(object):
......@@ -80,7 +81,8 @@ class TransactionManager(object):
return self._txn
def free(self, txn):
assert txn is self._txn
if txn is not self._txn:
raise ValueError("Foreign transaction")
self._txn = None
def registerSynch(self, synch):
......@@ -125,7 +127,8 @@ class TransactionManager(object):
return self.get().savepoint(optimistic)
def attempts(self, number=3):
assert number > 0
if number <= 0:
raise ValueError("number must be positive")
while number:
number -= 1
if number:
......@@ -149,6 +152,7 @@ class ThreadTransactionManager(TransactionManager, threading.local):
Each thread is associated with a unique transaction.
"""
class Attempt(object):
def __init__(self, manager):
......@@ -160,7 +164,7 @@ class Attempt(object):
if retry:
return retry # suppress the exception if necessary
reraise(t, v, tb) # otherwise reraise the exception
def __enter__(self):
return self.manager.__enter__()
......@@ -172,4 +176,3 @@ class Attempt(object):
return self._retry_or_raise(*sys.exc_info())
else:
return self._retry_or_raise(t, v, tb)
......@@ -11,92 +11,6 @@
# FOR A PARTICULAR PURPOSE.
#
############################################################################
"""Transaction objects manage resources for an individual activity.
Compatibility issues
--------------------
The implementation of Transaction objects involves two layers of
backwards compatibility, because this version of transaction supports
both ZODB 3 and ZODB 4. Zope is evolving towards the ZODB4
interfaces.
Transaction has two methods for a resource manager to call to
participate in a transaction -- register() and join(). join() takes a
resource manager and adds it to the list of resources. register() is
for backwards compatibility. It takes a persistent object and
registers its _p_jar attribute. TODO: explain adapter
Two-phase commit
----------------
A transaction commit involves an interaction between the transaction
object and one or more resource managers. The transaction manager
calls the following four methods on each resource manager; it calls
tpc_begin() on each resource manager before calling commit() on any of
them.
1. tpc_begin(txn)
2. commit(txn)
3. tpc_vote(txn)
4. tpc_finish(txn)
Before-commit hook
------------------
Sometimes, applications want to execute some code when a transaction is
committed. For example, one might want to delay object indexing until a
transaction commits, rather than indexing every time an object is changed.
Or someone might want to check invariants only after a set of operations. A
pre-commit hook is available for such use cases: use addBeforeCommitHook(),
passing it a callable and arguments. The callable will be called with its
arguments at the start of the commit (but not for substransaction commits).
After-commit hook
------------------
Sometimes, applications want to execute code after a transaction commit
attempt succeeds or aborts. For example, one might want to launch non
transactional code after a successful commit. Or still someone might
want to launch asynchronous code after. A post-commit hook is
available for such use cases: use addAfterCommitHook(), passing it a
callable and arguments. The callable will be called with a Boolean
value representing the status of the commit operation as first
argument (true if successfull or false iff aborted) preceding its
arguments at the start of the commit (but not for substransaction
commits). Commit hooks are not called for transaction.abort().
Error handling
--------------
When errors occur during two-phase commit, the transaction manager
aborts all the resource managers. The specific methods it calls
depend on whether the error occurs before or after the call to
tpc_vote() on that transaction manager.
If the resource manager has not voted, then the resource manager will
have one or more uncommitted objects. There are two cases that lead
to this state; either the transaction manager has not called commit()
for any objects on this resource manager or the call that failed was a
commit() for one of the objects of this resource manager. For each
uncommitted object, including the object that failed in its commit(),
call abort().
Once uncommitted objects are aborted, tpc_abort() or abort_sub() is
called on each resource manager.
Synchronization
---------------
You can register sychronization objects (synchronizers) with the
tranasction manager. The synchronizer must implement
beforeCompletion() and afterCompletion() methods. The transaction
manager calls beforeCompletion() when it starts a top-level two-phase
commit. It calls afterCompletion() when a top-level transaction is
committed or aborted. The methods are passed the current Transaction
as their only argument.
"""
import binascii
import logging
import sys
......@@ -105,17 +19,30 @@ import traceback
from zope.interface import implementer
from transaction.compat import reraise
from transaction.compat import get_thread_ident
from transaction.compat import native_
from transaction.compat import bytes_
from transaction.compat import StringIO
from transaction.weakset import WeakSet
from transaction.interfaces import TransactionFailedError
from transaction import interfaces
from transaction._compat import reraise
from transaction._compat import get_thread_ident
from transaction._compat import native_
from transaction._compat import bytes_
from transaction._compat import StringIO
_marker = object()
_TB_BUFFER = None #unittests may hook
def _makeTracebackBuffer(): #pragma NO COVER
if _TB_BUFFER is not None:
return _TB_BUFFER
return StringIO()
_LOGGER = None #unittests may hook
def _makeLogger(): #pragma NO COVER
if _LOGGER is not None:
return _LOGGER
return logging.getLogger("txn.%d" % get_thread_ident())
# The point of this is to avoid hiding exceptions (which the builtin
# hasattr() does).
def myhasattr(obj, attr):
......@@ -177,7 +104,7 @@ class Transaction(object):
# directly by storages, leading underscore notwithstanding.
self._extension = {}
self.log = logging.getLogger("txn.%d" % get_thread_ident())
self.log = _makeLogger()
self.log.debug("new transaction")
# If a commit fails, the traceback is saved in _failure_traceback.
......@@ -203,7 +130,7 @@ class Transaction(object):
if self.status is not Status.ACTIVE:
# should not doom transactions in the middle,
# or after, a commit
raise AssertionError()
raise ValueError('non-doomable')
self.status = Status.DOOMED
# Raise TransactionFailedError, due to commit()/join()/register()
......@@ -307,7 +234,6 @@ class Transaction(object):
# be stored when the transaction commits. For other
# objects, the object implements the standard two-phase
# commit protocol.
manager = getattr(obj, "_p_jar", obj)
if manager is None:
raise ValueError("Register with no manager")
......@@ -364,7 +290,7 @@ class Transaction(object):
def _saveAndGetCommitishError(self):
self.status = Status.COMMITFAILED
# Save the traceback for TransactionFailedError.
ft = self._failure_traceback = StringIO()
ft = self._failure_traceback = _makeTracebackBuffer()
t = None
v = None
tb = None
......@@ -379,7 +305,6 @@ class Transaction(object):
return t, v, tb
finally:
del t, v, tb
def _saveAndRaiseCommitishError(self):
t = None
......@@ -390,7 +315,6 @@ class Transaction(object):
reraise(t, v, tb)
finally:
del t, v, tb
def getBeforeCommitHooks(self):
""" See ITransaction.
......@@ -566,6 +490,7 @@ class Transaction(object):
# TODO: We need a better name for the adapters.
class MultiObjectResourceAdapter(object):
"""Adapt the old-style register() call to the new-style join().
......@@ -573,7 +498,6 @@ class MultiObjectResourceAdapter(object):
the transaction manager. With register(), an individual object
is passed to register().
"""
def __init__(self, jar):
self.manager = jar
self.objects = []
......@@ -624,6 +548,7 @@ class MultiObjectResourceAdapter(object):
finally:
del t, v, tb
def rm_key(rm):
func = getattr(rm, 'sortKey', None)
if func is not None:
......@@ -634,13 +559,14 @@ def object_hint(o):
This function does not raise an exception.
"""
# We should always be able to get __class__.
klass = o.__class__.__name__
# oid would be great, but may this isn't a persistent object.
# oid would be great, but maybe this isn't a persistent object.
oid = getattr(o, "_p_oid", _marker)
if oid is not _marker:
oid = oid_repr(oid)
else:
oid = 'None'
return "%s oid=%s" % (klass, oid)
def oid_repr(oid):
......@@ -657,6 +583,7 @@ def oid_repr(oid):
else:
return repr(oid)
# TODO: deprecate for 3.6.
class DataManagerAdapter(object):
"""Adapt zodb 4-style data managers to zodb3 style
......@@ -700,6 +627,7 @@ class DataManagerAdapter(object):
def sortKey(self):
return self._datamanager.sortKey()
@implementer(interfaces.ISavepoint)
class Savepoint:
"""Transaction savepoint.
......@@ -742,6 +670,7 @@ class Savepoint:
# Mark the transaction as failed.
transaction._saveAndRaiseCommitishError() # reraises!
class AbortSavepoint:
def __init__(self, datamanager, transaction):
......@@ -752,6 +681,7 @@ class AbortSavepoint:
self.datamanager.abort(self.transaction)
self.transaction._unjoin(self.datamanager)
class NoRollbackSavepoint:
def __init__(self, datamanager):
......
##############################################################################
#
# Copyright (c) 2012 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
class DummyFile(object):
def __init__(self):
self._lines = []
def write(self, text):
self._lines.append(text)
def writelines(self, lines):
self._lines.extend(lines)
class DummyLogger(object):
def __init__(self):
self._clear()
def _clear(self):
self._log = []
def log(self, level, msg, *args, **kw):
if args:
self._log.append((level, msg % args))
elif kw:
self._log.append((level, msg % kw))
else:
self._log.append((level, msg))
def debug(self, msg, *args, **kw):
self.log('debug', msg, *args, **kw)
def error(self, msg, *args, **kw):
self.log('error', msg, *args, **kw)
def critical(self, msg, *args, **kw):
self.log('critical', msg, *args, **kw)
class Monkey(object):
# context-manager for replacing module names in the scope of a test.
def __init__(self, module, **kw):
self.module = module
self.to_restore = dict([(key, getattr(module, key)) for key in kw])
for key, value in kw.items():
setattr(module, key, value)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
for key, value in self.to_restore.items():
setattr(self.module, key, value)
def assertRaisesEx(e_type, checked, *args, **kw):
try:
checked(*args, **kw)
except e_type as e:
return e
raise AssertionError("Didn't raise: %s" % e_type.__name__)
##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Sample objects for use in tests
"""
class DataManager(object):
"""Sample data manager
Used by the 'datamanager' chapter in the Sphinx docs.
"""
def __init__(self):
self.state = 0
self.sp = 0
self.transaction = None
self.delta = 0
self.prepared = False
def inc(self, n=1):
self.delta += n
def prepare(self, transaction):
if self.prepared:
raise TypeError('Already prepared')
self._checkTransaction(transaction)
self.prepared = True
self.transaction = transaction
self.state += self.delta
def _checkTransaction(self, transaction):
if (transaction is not self.transaction
and self.transaction is not None):
raise TypeError("Transaction missmatch",
transaction, self.transaction)
def abort(self, transaction):
self._checkTransaction(transaction)
if self.transaction is not None:
self.transaction = None
if self.prepared:
self.state -= self.delta
self.prepared = False
self.delta = 0
def commit(self, transaction):
if not self.prepared:
raise TypeError('Not prepared to commit')
self._checkTransaction(transaction)
self.delta = 0
self.transaction = None
self.prepared = False
def savepoint(self, transaction):
if self.prepared:
raise TypeError("Can't get savepoint during two-phase commit")
self._checkTransaction(transaction)
self.transaction = transaction
self.sp += 1
return Rollback(self)
class Rollback(object):
def __init__(self, dm):
self.dm = dm
self.sp = dm.sp
self.delta = dm.delta
self.transaction = dm.transaction
def rollback(self):
if self.transaction is not self.dm.transaction:
raise TypeError("Attempt to rollback stale rollback")
if self.dm.sp < self.sp:
raise TypeError("Attempt to roll back to invalid save point",
self.sp, self.dm.sp)
self.dm.sp = self.sp
self.dm.delta = self.delta
class ResourceManager(object):
""" Sample resource manager.
Used by the 'resourcemanager' chapter in the Sphinx docs.
"""
def __init__(self):
self.state = 0
self.sp = 0
self.transaction = None
self.delta = 0
self.txn_state = None
def _check_state(self, *ok_states):
if self.txn_state not in ok_states:
raise ValueError("txn in state %r but expected one of %r" %
(self.txn_state, ok_states))
def _checkTransaction(self, transaction):
if (transaction is not self.transaction
and self.transaction is not None):
raise TypeError("Transaction missmatch",
transaction, self.transaction)
def inc(self, n=1):
self.delta += n
def tpc_begin(self, transaction):
self._checkTransaction(transaction)
self._check_state(None)
self.transaction = transaction
self.txn_state = 'tpc_begin'
def tpc_vote(self, transaction):
self._checkTransaction(transaction)
self._check_state('tpc_begin')
self.state += self.delta
self.txn_state = 'tpc_vote'
def tpc_finish(self, transaction):
self._checkTransaction(transaction)
self._check_state('tpc_vote')
self.delta = 0
self.transaction = None
self.prepared = False
self.txn_state = None
def tpc_abort(self, transaction):
self._checkTransaction(transaction)
if self.transaction is not None:
self.transaction = None
if self.txn_state == 'tpc_vote':
self.state -= self.delta
self.txn_state = None
self.delta = 0
def savepoint(self, transaction):
if self.txn_state is not None:
raise TypeError("Can't get savepoint during two-phase commit")
self._checkTransaction(transaction)
self.transaction = transaction
self.sp += 1
return SavePoint(self)
def discard(self, transaction):
pass
class SavePoint(object):
def __init__(self, rm):
self.rm = rm
self.sp = rm.sp
self.delta = rm.delta
self.transaction = rm.transaction
def rollback(self):
if self.transaction is not self.rm.transaction:
raise TypeError("Attempt to rollback stale rollback")
if self.rm.sp < self.sp:
raise TypeError("Attempt to roll back to invalid save point",
self.sp, self.rm.sp)
self.rm.sp = self.sp
self.rm.delta = self.delta
def discard(self):
pass
##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Sample objects for use in tests
$Id: sampledm.py 29896 2005-04-07 04:48:06Z tim_one $
"""
class DataManager(object):
"""Sample data manager
This class provides a trivial data-manager implementation and doc
strings to illustrate the the protocol and to provide a tool for
writing tests.
Our sample data manager has state that is updated through an inc
method and through transaction operations.
When we create a sample data manager:
>>> dm = DataManager()
It has two bits of state, state:
>>> dm.state
0
and delta:
>>> dm.delta
0
Both of which are initialized to 0. state is meant to model
committed state, while delta represents tentative changes within a
transaction. We change the state by calling inc:
>>> dm.inc()
which updates delta:
>>> dm.delta
1
but state isn't changed until we commit the transaction:
>>> dm.state
0
To commit the changes, we use 2-phase commit. We execute the first
stage by calling prepare. We need to pass a transation. Our
sample data managers don't really use the transactions for much,
so we'll be lazy and use strings for transactions:
>>> t1 = '1'
>>> dm.prepare(t1)
The sample data manager updates the state when we call prepare:
>>> dm.state
1
>>> dm.delta
1
This is mainly so we can detect some affect of calling the methods.
Now if we call commit:
>>> dm.commit(t1)
Our changes are"permanent". The state reflects the changes and the
delta has been reset to 0.
>>> dm.state
1
>>> dm.delta
0
"""
def __init__(self):
self.state = 0
self.sp = 0
self.transaction = None
self.delta = 0
self.prepared = False
def inc(self, n=1):
self.delta += n
def prepare(self, transaction):
"""Prepare to commit data
>>> dm = DataManager()
>>> dm.inc()
>>> t1 = '1'
>>> dm.prepare(t1)
>>> dm.commit(t1)
>>> dm.state
1
>>> dm.inc()
>>> t2 = '2'
>>> dm.prepare(t2)
>>> dm.abort(t2)
>>> dm.state
1
It is en error to call prepare more than once without an intervening
commit or abort:
>>> dm.prepare(t1)
>>> dm.prepare(t1)
Traceback (most recent call last):
...
TypeError: Already prepared
>>> dm.prepare(t2)
Traceback (most recent call last):
...
TypeError: Already prepared
>>> dm.abort(t1)
If there was a preceeding savepoint, the transaction must match:
>>> rollback = dm.savepoint(t1)
>>> dm.prepare(t2)
Traceback (most recent call last):
,,,
TypeError: ('Transaction missmatch', '2', '1')
>>> dm.prepare(t1)
"""
if self.prepared:
raise TypeError('Already prepared')
self._checkTransaction(transaction)
self.prepared = True
self.transaction = transaction
self.state += self.delta
def _checkTransaction(self, transaction):
if (transaction is not self.transaction
and self.transaction is not None):
raise TypeError("Transaction missmatch",
transaction, self.transaction)
def abort(self, transaction):
"""Abort a transaction
The abort method can be called before two-phase commit to
throw away work done in the transaction:
>>> dm = DataManager()
>>> dm.inc()
>>> dm.state, dm.delta
(0, 1)
>>> t1 = '1'
>>> dm.abort(t1)
>>> dm.state, dm.delta
(0, 0)
The abort method also throws away work done in savepoints:
>>> dm.inc()
>>> r = dm.savepoint(t1)
>>> dm.inc()
>>> r = dm.savepoint(t1)
>>> dm.state, dm.delta
(0, 2)
>>> dm.abort(t1)
>>> dm.state, dm.delta
(0, 0)
If savepoints are used, abort must be passed the same
transaction:
>>> dm.inc()
>>> r = dm.savepoint(t1)
>>> t2 = '2'
>>> dm.abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> dm.abort(t1)
The abort method is also used to abort a two-phase commit:
>>> dm.inc()
>>> dm.state, dm.delta
(0, 1)
>>> dm.prepare(t1)
>>> dm.state, dm.delta
(1, 1)
>>> dm.abort(t1)
>>> dm.state, dm.delta
(0, 0)
Of course, the transactions passed to prepare and abort must
match:
>>> dm.prepare(t1)
>>> dm.abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> dm.abort(t1)
"""
self._checkTransaction(transaction)
if self.transaction is not None:
self.transaction = None
if self.prepared:
self.state -= self.delta
self.prepared = False
self.delta = 0
def commit(self, transaction):
"""Complete two-phase commit
>>> dm = DataManager()
>>> dm.state
0
>>> dm.inc()
We start two-phase commit by calling prepare:
>>> t1 = '1'
>>> dm.prepare(t1)
We complete it by calling commit:
>>> dm.commit(t1)
>>> dm.state
1
It is an error ro call commit without calling prepare first:
>>> dm.inc()
>>> t2 = '2'
>>> dm.commit(t2)
Traceback (most recent call last):
...
TypeError: Not prepared to commit
>>> dm.prepare(t2)
>>> dm.commit(t2)
If course, the transactions given to prepare and commit must
be the same:
>>> dm.inc()
>>> t3 = '3'
>>> dm.prepare(t3)
>>> dm.commit(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '3')
"""
if not self.prepared:
raise TypeError('Not prepared to commit')
self._checkTransaction(transaction)
self.delta = 0
self.transaction = None
self.prepared = False
def savepoint(self, transaction):
"""Provide the ability to rollback transaction state
Savepoints provide a way to:
- Save partial transaction work. For some data managers, this
could allow resources to be used more efficiently.
- Provide the ability to revert state to a point in a
transaction without aborting the entire transaction. In
other words, savepoints support partial aborts.
Savepoints don't use two-phase commit. If there are errors in
setting or rolling back to savepoints, the application should
abort the containing transaction. This is *not* the
responsibility of the data manager.
Savepoints are always associated with a transaction. Any work
done in a savepoint's transaction is tentative until the
transaction is committed using two-phase commit.
>>> dm = DataManager()
>>> dm.inc()
>>> t1 = '1'
>>> r = dm.savepoint(t1)
>>> dm.state, dm.delta
(0, 1)
>>> dm.inc()
>>> dm.state, dm.delta
(0, 2)
>>> r.rollback()
>>> dm.state, dm.delta
(0, 1)
>>> dm.prepare(t1)
>>> dm.commit(t1)
>>> dm.state, dm.delta
(1, 0)
Savepoints must have the same transaction:
>>> r1 = dm.savepoint(t1)
>>> dm.state, dm.delta
(1, 0)
>>> dm.inc()
>>> dm.state, dm.delta
(1, 1)
>>> t2 = '2'
>>> r2 = dm.savepoint(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> r2 = dm.savepoint(t1)
>>> dm.inc()
>>> dm.state, dm.delta
(1, 2)
If we rollback to an earlier savepoint, we discard all work
done later:
>>> r1.rollback()
>>> dm.state, dm.delta
(1, 0)
and we can no longer rollback to the later savepoint:
>>> r2.rollback()
Traceback (most recent call last):
...
TypeError: ('Attempt to roll back to invalid save point', 3, 2)
We can roll back to a savepoint as often as we like:
>>> r1.rollback()
>>> r1.rollback()
>>> r1.rollback()
>>> dm.state, dm.delta
(1, 0)
>>> dm.inc()
>>> dm.inc()
>>> dm.inc()
>>> dm.state, dm.delta
(1, 3)
>>> r1.rollback()
>>> dm.state, dm.delta
(1, 0)
But we can't rollback to a savepoint after it has been
committed:
>>> dm.prepare(t1)
>>> dm.commit(t1)
>>> r1.rollback()
Traceback (most recent call last):
...
TypeError: Attempt to rollback stale rollback
"""
if self.prepared:
raise TypeError("Can't get savepoint during two-phase commit")
self._checkTransaction(transaction)
self.transaction = transaction
self.sp += 1
return Rollback(self)
class Rollback(object):
def __init__(self, dm):
self.dm = dm
self.sp = dm.sp
self.delta = dm.delta
self.transaction = dm.transaction
def rollback(self):
if self.transaction is not self.dm.transaction:
raise TypeError("Attempt to rollback stale rollback")
if self.dm.sp < self.sp:
raise TypeError("Attempt to roll back to invalid save point",
self.sp, self.dm.sp)
self.dm.sp = self.sp
self.dm.delta = self.delta
def test_suite():
from doctest import DocTestSuite
return DocTestSuite()
if __name__ == '__main__':
unittest.main()
......@@ -16,7 +16,7 @@
Sample data manager implementation that illustrates how to implement
savepoints.
See savepoint.txt in the transaction package.
Used by savepoint.rst in the Sphinx docs.
"""
from zope.interface import implementer
......
##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Sample objects for use in tests
"""
from doctest import DocTestSuite
class DataManager(object):
"""Sample data manager
This class provides a trivial data-manager implementation and doc
strings to illustrate the the protocol and to provide a tool for
writing tests.
Our sample data manager has state that is updated through an inc
method and through transaction operations.
When we create a sample data manager:
>>> dm = DataManager()
It has two bits of state, state:
>>> dm.state
0
and delta:
>>> dm.delta
0
Both of which are initialized to 0. state is meant to model
committed state, while delta represents tentative changes within a
transaction. We change the state by calling inc:
>>> dm.inc()
which updates delta:
>>> dm.delta
1
but state isn't changed until we commit the transaction:
>>> dm.state
0
To commit the changes, we use 2-phase commit. We execute the first
stage by calling prepare. We need to pass a transation. Our
sample data managers don't really use the transactions for much,
so we'll be lazy and use strings for transactions:
>>> t1 = '1'
>>> dm.prepare(t1)
The sample data manager updates the state when we call prepare:
>>> dm.state
1
>>> dm.delta
1
This is mainly so we can detect some affect of calling the methods.
Now if we call commit:
>>> dm.commit(t1)
Our changes are"permanent". The state reflects the changes and the
delta has been reset to 0.
>>> dm.state
1
>>> dm.delta
0
"""
def __init__(self):
self.state = 0
self.sp = 0
self.transaction = None
self.delta = 0
self.prepared = False
def inc(self, n=1):
self.delta += n
def prepare(self, transaction):
"""Prepare to commit data
>>> dm = DataManager()
>>> dm.inc()
>>> t1 = '1'
>>> dm.prepare(t1)
>>> dm.commit(t1)
>>> dm.state
1
>>> dm.inc()
>>> t2 = '2'
>>> dm.prepare(t2)
>>> dm.abort(t2)
>>> dm.state
1
It is en error to call prepare more than once without an intervening
commit or abort:
>>> dm.prepare(t1)
>>> dm.prepare(t1)
Traceback (most recent call last):
...
TypeError: Already prepared
>>> dm.prepare(t2)
Traceback (most recent call last):
...
TypeError: Already prepared
>>> dm.abort(t1)
If there was a preceeding savepoint, the transaction must match:
>>> rollback = dm.savepoint(t1)
>>> dm.prepare(t2)
Traceback (most recent call last):
,,,
TypeError: ('Transaction missmatch', '2', '1')
>>> dm.prepare(t1)
"""
if self.prepared:
raise TypeError('Already prepared')
self._checkTransaction(transaction)
self.prepared = True
self.transaction = transaction
self.state += self.delta
def _checkTransaction(self, transaction):
if (transaction is not self.transaction
and self.transaction is not None):
raise TypeError("Transaction missmatch",
transaction, self.transaction)
def abort(self, transaction):
"""Abort a transaction
The abort method can be called before two-phase commit to
throw away work done in the transaction:
>>> dm = DataManager()
>>> dm.inc()
>>> dm.state, dm.delta
(0, 1)
>>> t1 = '1'
>>> dm.abort(t1)
>>> dm.state, dm.delta
(0, 0)
The abort method also throws away work done in savepoints:
>>> dm.inc()
>>> r = dm.savepoint(t1)
>>> dm.inc()
>>> r = dm.savepoint(t1)
>>> dm.state, dm.delta
(0, 2)
>>> dm.abort(t1)
>>> dm.state, dm.delta
(0, 0)
If savepoints are used, abort must be passed the same
transaction:
>>> dm.inc()
>>> r = dm.savepoint(t1)
>>> t2 = '2'
>>> dm.abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> dm.abort(t1)
The abort method is also used to abort a two-phase commit:
>>> dm.inc()
>>> dm.state, dm.delta
(0, 1)
>>> dm.prepare(t1)
>>> dm.state, dm.delta
(1, 1)
>>> dm.abort(t1)
>>> dm.state, dm.delta
(0, 0)
Of course, the transactions passed to prepare and abort must
match:
>>> dm.prepare(t1)
>>> dm.abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> dm.abort(t1)
"""
self._checkTransaction(transaction)
if self.transaction is not None:
self.transaction = None
if self.prepared:
self.state -= self.delta
self.prepared = False
self.delta = 0
def commit(self, transaction):
"""Complete two-phase commit
>>> dm = DataManager()
>>> dm.state
0
>>> dm.inc()
We start two-phase commit by calling prepare:
>>> t1 = '1'
>>> dm.prepare(t1)
We complete it by calling commit:
>>> dm.commit(t1)
>>> dm.state
1
It is an error ro call commit without calling prepare first:
>>> dm.inc()
>>> t2 = '2'
>>> dm.commit(t2)
Traceback (most recent call last):
...
TypeError: Not prepared to commit
>>> dm.prepare(t2)
>>> dm.commit(t2)
If course, the transactions given to prepare and commit must
be the same:
>>> dm.inc()
>>> t3 = '3'
>>> dm.prepare(t3)
>>> dm.commit(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '3')
"""
if not self.prepared:
raise TypeError('Not prepared to commit')
self._checkTransaction(transaction)
self.delta = 0
self.transaction = None
self.prepared = False
def savepoint(self, transaction):
"""Provide the ability to rollback transaction state
Savepoints provide a way to:
- Save partial transaction work. For some data managers, this
could allow resources to be used more efficiently.
- Provide the ability to revert state to a point in a
transaction without aborting the entire transaction. In
other words, savepoints support partial aborts.
Savepoints don't use two-phase commit. If there are errors in
setting or rolling back to savepoints, the application should
abort the containing transaction. This is *not* the
responsibility of the data manager.
Savepoints are always associated with a transaction. Any work
done in a savepoint's transaction is tentative until the
transaction is committed using two-phase commit.
>>> dm = DataManager()
>>> dm.inc()
>>> t1 = '1'
>>> r = dm.savepoint(t1)
>>> dm.state, dm.delta
(0, 1)
>>> dm.inc()
>>> dm.state, dm.delta
(0, 2)
>>> r.rollback()
>>> dm.state, dm.delta
(0, 1)
>>> dm.prepare(t1)
>>> dm.commit(t1)
>>> dm.state, dm.delta
(1, 0)
Savepoints must have the same transaction:
>>> r1 = dm.savepoint(t1)
>>> dm.state, dm.delta
(1, 0)
>>> dm.inc()
>>> dm.state, dm.delta
(1, 1)
>>> t2 = '2'
>>> r2 = dm.savepoint(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> r2 = dm.savepoint(t1)
>>> dm.inc()
>>> dm.state, dm.delta
(1, 2)
If we rollback to an earlier savepoint, we discard all work
done later:
>>> r1.rollback()
>>> dm.state, dm.delta
(1, 0)
and we can no longer rollback to the later savepoint:
>>> r2.rollback()
Traceback (most recent call last):
...
TypeError: ('Attempt to roll back to invalid save point', 3, 2)
We can roll back to a savepoint as often as we like:
>>> r1.rollback()
>>> r1.rollback()
>>> r1.rollback()
>>> dm.state, dm.delta
(1, 0)
>>> dm.inc()
>>> dm.inc()
>>> dm.inc()
>>> dm.state, dm.delta
(1, 3)
>>> r1.rollback()
>>> dm.state, dm.delta
(1, 0)
But we can't rollback to a savepoint after it has been
committed:
>>> dm.prepare(t1)
>>> dm.commit(t1)
>>> r1.rollback()
Traceback (most recent call last):
...
TypeError: Attempt to rollback stale rollback
"""
if self.prepared:
raise TypeError("Can't get savepoint during two-phase commit")
self._checkTransaction(transaction)
self.transaction = transaction
self.sp += 1
return Rollback(self)
class Rollback(object):
def __init__(self, dm):
self.dm = dm
self.sp = dm.sp
self.delta = dm.delta
self.transaction = dm.transaction
def rollback(self):
if self.transaction is not self.dm.transaction:
raise TypeError("Attempt to rollback stale rollback")
if self.dm.sp < self.sp:
raise TypeError("Attempt to roll back to invalid save point",
self.sp, self.dm.sp)
self.dm.sp = self.sp
self.dm.delta = self.delta
def test_suite():
return DocTestSuite()
# additional_tests is for setuptools "setup.py test" support
additional_tests = test_suite
if __name__ == '__main__':
unittest.main()
##############################################################################
#
# Copyright (c) 2004 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Sample objects for use in tests
$Id$
"""
class ResourceManager(object):
"""Sample resource manager.
This class provides a trivial resource-manager implementation and doc
strings to illustrate the protocol and to provide a tool for writing
tests.
Our sample resource manager has state that is updated through an inc
method and through transaction operations.
When we create a sample resource manager:
>>> rm = ResourceManager()
It has two pieces state, state and delta, both initialized to 0:
>>> rm.state
0
>>> rm.delta
0
state is meant to model committed state, while delta represents
tentative changes within a transaction. We change the state by
calling inc:
>>> rm.inc()
which updates delta:
>>> rm.delta
1
but state isn't changed until we commit the transaction:
>>> rm.state
0
To commit the changes, we use 2-phase commit. We execute the first
stage by calling prepare. We need to pass a transation. Our
sample resource managers don't really use the transactions for much,
so we'll be lazy and use strings for transactions. The sample
resource manager updates the state when we call tpc_vote:
>>> t1 = '1'
>>> rm.tpc_begin(t1)
>>> rm.state, rm.delta
(0, 1)
>>> rm.tpc_vote(t1)
>>> rm.state, rm.delta
(1, 1)
Now if we call tpc_finish:
>>> rm.tpc_finish(t1)
Our changes are "permanent". The state reflects the changes and the
delta has been reset to 0.
>>> rm.state, rm.delta
(1, 0)
"""
def __init__(self):
self.state = 0
self.sp = 0
self.transaction = None
self.delta = 0
self.txn_state = None
def _check_state(self, *ok_states):
if self.txn_state not in ok_states:
raise ValueError("txn in state %r but expected one of %r" %
(self.txn_state, ok_states))
def _checkTransaction(self, transaction):
if (transaction is not self.transaction
and self.transaction is not None):
raise TypeError("Transaction missmatch",
transaction, self.transaction)
def inc(self, n=1):
self.delta += n
def tpc_begin(self, transaction):
"""Prepare to commit data.
>>> rm = ResourceManager()
>>> rm.inc()
>>> t1 = '1'
>>> rm.tpc_begin(t1)
>>> rm.tpc_vote(t1)
>>> rm.tpc_finish(t1)
>>> rm.state
1
>>> rm.inc()
>>> t2 = '2'
>>> rm.tpc_begin(t2)
>>> rm.tpc_vote(t2)
>>> rm.tpc_abort(t2)
>>> rm.state
1
It is an error to call tpc_begin more than once without completing
two-phase commit:
>>> rm.tpc_begin(t1)
>>> rm.tpc_begin(t1)
Traceback (most recent call last):
...
ValueError: txn in state 'tpc_begin' but expected one of (None,)
>>> rm.tpc_abort(t1)
If there was a preceeding savepoint, the transaction must match:
>>> rollback = rm.savepoint(t1)
>>> rm.tpc_begin(t2)
Traceback (most recent call last):
,,,
TypeError: ('Transaction missmatch', '2', '1')
>>> rm.tpc_begin(t1)
"""
self._checkTransaction(transaction)
self._check_state(None)
self.transaction = transaction
self.txn_state = 'tpc_begin'
def tpc_vote(self, transaction):
"""Verify that a data manager can commit the transaction.
This is the last chance for a data manager to vote 'no'. A
data manager votes 'no' by raising an exception.
transaction is the ITransaction instance associated with the
transaction being committed.
"""
self._checkTransaction(transaction)
self._check_state('tpc_begin')
self.state += self.delta
self.txn_state = 'tpc_vote'
def tpc_finish(self, transaction):
"""Complete two-phase commit
>>> rm = ResourceManager()
>>> rm.state
0
>>> rm.inc()
We start two-phase commit by calling prepare:
>>> t1 = '1'
>>> rm.tpc_begin(t1)
>>> rm.tpc_vote(t1)
We complete it by calling tpc_finish:
>>> rm.tpc_finish(t1)
>>> rm.state
1
It is an error ro call tpc_finish without calling tpc_vote:
>>> rm.inc()
>>> t2 = '2'
>>> rm.tpc_begin(t2)
>>> rm.tpc_finish(t2)
Traceback (most recent call last):
...
ValueError: txn in state 'tpc_begin' but expected one of ('tpc_vote',)
>>> rm.tpc_abort(t2) # clean slate
>>> rm.tpc_begin(t2)
>>> rm.tpc_vote(t2)
>>> rm.tpc_finish(t2)
Of course, the transactions given to tpc_begin and tpc_finish must
be the same:
>>> rm.inc()
>>> t3 = '3'
>>> rm.tpc_begin(t3)
>>> rm.tpc_vote(t3)
>>> rm.tpc_finish(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '3')
"""
self._checkTransaction(transaction)
self._check_state('tpc_vote')
self.delta = 0
self.transaction = None
self.prepared = False
self.txn_state = None
def tpc_abort(self, transaction):
"""Abort a transaction
The abort method can be called before two-phase commit to
throw away work done in the transaction:
>>> rm = ResourceManager()
>>> rm.inc()
>>> rm.state, rm.delta
(0, 1)
>>> t1 = '1'
>>> rm.tpc_abort(t1)
>>> rm.state, rm.delta
(0, 0)
The abort method also throws away work done in savepoints:
>>> rm.inc()
>>> r = rm.savepoint(t1)
>>> rm.inc()
>>> r = rm.savepoint(t1)
>>> rm.state, rm.delta
(0, 2)
>>> rm.tpc_abort(t1)
>>> rm.state, rm.delta
(0, 0)
If savepoints are used, abort must be passed the same
transaction:
>>> rm.inc()
>>> r = rm.savepoint(t1)
>>> t2 = '2'
>>> rm.tpc_abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> rm.tpc_abort(t1)
The abort method is also used to abort a two-phase commit:
>>> rm.inc()
>>> rm.state, rm.delta
(0, 1)
>>> rm.tpc_begin(t1)
>>> rm.state, rm.delta
(0, 1)
>>> rm.tpc_vote(t1)
>>> rm.state, rm.delta
(1, 1)
>>> rm.tpc_abort(t1)
>>> rm.state, rm.delta
(0, 0)
Of course, the transactions passed to prepare and abort must
match:
>>> rm.tpc_begin(t1)
>>> rm.tpc_abort(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> rm.tpc_abort(t1)
This should never fail.
"""
self._checkTransaction(transaction)
if self.transaction is not None:
self.transaction = None
if self.txn_state == 'tpc_vote':
self.state -= self.delta
self.txn_state = None
self.delta = 0
def savepoint(self, transaction):
"""Provide the ability to rollback transaction state
Savepoints provide a way to:
- Save partial transaction work. For some resource managers, this
could allow resources to be used more efficiently.
- Provide the ability to revert state to a point in a
transaction without aborting the entire transaction. In
other words, savepoints support partial aborts.
Savepoints don't use two-phase commit. If there are errors in
setting or rolling back to savepoints, the application should
abort the containing transaction. This is *not* the
responsibility of the resource manager.
Savepoints are always associated with a transaction. Any work
done in a savepoint's transaction is tentative until the
transaction is committed using two-phase commit.
>>> rm = ResourceManager()
>>> rm.inc()
>>> t1 = '1'
>>> r = rm.savepoint(t1)
>>> rm.state, rm.delta
(0, 1)
>>> rm.inc()
>>> rm.state, rm.delta
(0, 2)
>>> r.rollback()
>>> rm.state, rm.delta
(0, 1)
>>> rm.tpc_begin(t1)
>>> rm.tpc_vote(t1)
>>> rm.tpc_finish(t1)
>>> rm.state, rm.delta
(1, 0)
Savepoints must have the same transaction:
>>> r1 = rm.savepoint(t1)
>>> rm.state, rm.delta
(1, 0)
>>> rm.inc()
>>> rm.state, rm.delta
(1, 1)
>>> t2 = '2'
>>> r2 = rm.savepoint(t2)
Traceback (most recent call last):
...
TypeError: ('Transaction missmatch', '2', '1')
>>> r2 = rm.savepoint(t1)
>>> rm.inc()
>>> rm.state, rm.delta
(1, 2)
If we rollback to an earlier savepoint, we discard all work
done later:
>>> r1.rollback()
>>> rm.state, rm.delta
(1, 0)
and we can no longer rollback to the later savepoint:
>>> r2.rollback()
Traceback (most recent call last):
...
TypeError: ('Attempt to roll back to invalid save point', 3, 2)
We can roll back to a savepoint as often as we like:
>>> r1.rollback()
>>> r1.rollback()
>>> r1.rollback()
>>> rm.state, rm.delta
(1, 0)
>>> rm.inc()
>>> rm.inc()
>>> rm.inc()
>>> rm.state, rm.delta
(1, 3)
>>> r1.rollback()
>>> rm.state, rm.delta
(1, 0)
But we can't rollback to a savepoint after it has been
committed:
>>> rm.tpc_begin(t1)
>>> rm.tpc_vote(t1)
>>> rm.tpc_finish(t1)
>>> r1.rollback()
Traceback (most recent call last):
...
TypeError: Attempt to rollback stale rollback
"""
if self.txn_state is not None:
raise TypeError("Can't get savepoint during two-phase commit")
self._checkTransaction(transaction)
self.transaction = transaction
self.sp += 1
return SavePoint(self)
def discard(self, transaction):
pass
class SavePoint(object):
def __init__(self, rm):
self.rm = rm
self.sp = rm.sp
self.delta = rm.delta
self.transaction = rm.transaction
def rollback(self):
if self.transaction is not self.rm.transaction:
raise TypeError("Attempt to rollback stale rollback")
if self.rm.sp < self.sp:
raise TypeError("Attempt to roll back to invalid save point",
self.sp, self.rm.sp)
self.rm.sp = self.sp
self.rm.delta = self.delta
def discard(self):
pass
def test_suite():
from doctest import DocTestSuite
return DocTestSuite()
# additional_tests is for setuptools "setup.py test" support
additional_tests = test_suite
if __name__ == '__main__':
unittest.main()
##############################################################################
#
# Copyright (c) 2012 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
import unittest
class TransactionManagerTests(unittest.TestCase):
def _getTargetClass(self):
from transaction import TransactionManager
return TransactionManager
def _makeOne(self):
return self._getTargetClass()()
def _makePopulated(self):
mgr = self._makeOne()
sub1 = DataObject(mgr)
sub2 = DataObject(mgr)
sub3 = DataObject(mgr)
nosub1 = DataObject(mgr, nost=1)
return mgr, sub1, sub2, sub3, nosub1
def test_ctor(self):
tm = self._makeOne()
self.assertTrue(tm._txn is None)
self.assertEqual(len(tm._synchs), 0)
def test_begin_wo_existing_txn_wo_synchs(self):
from transaction._transaction import Transaction
tm = self._makeOne()
tm.begin()
self.assertTrue(isinstance(tm._txn, Transaction))
def test_begin_wo_existing_txn_w_synchs(self):
from transaction._transaction import Transaction
tm = self._makeOne()
synch = DummySynch()
tm.registerSynch(synch)
tm.begin()
self.assertTrue(isinstance(tm._txn, Transaction))
self.assertTrue(tm._txn in synch._txns)
def test_begin_w_existing_txn(self):
class Existing(object):
_aborted = False
def abort(self):
self._aborted = True
tm = self._makeOne()
tm._txn = txn = Existing()
tm.begin()
self.assertFalse(tm._txn is txn)
self.assertTrue(txn._aborted)
def test_get_wo_existing_txn(self):
from transaction._transaction import Transaction
tm = self._makeOne()
txn = tm.get()
self.assertTrue(isinstance(txn, Transaction))
def test_get_w_existing_txn(self):
class Existing(object):
_aborted = False
def abort(self):
self._aborted = True
tm = self._makeOne()
tm._txn = txn = Existing()
self.assertTrue(tm.get() is txn)
def test_free_w_other_txn(self):
from transaction._transaction import Transaction
tm = self._makeOne()
txn = Transaction()
tm.begin()
self.assertRaises(ValueError, tm.free, txn)
def test_free_w_existing_txn(self):
class Existing(object):
_aborted = False
def abort(self):
self._aborted = True
tm = self._makeOne()
tm._txn = txn = Existing()
tm.free(txn)
self.assertTrue(tm._txn is None)
def test_registerSynch(self):
tm = self._makeOne()
synch = DummySynch()
tm.registerSynch(synch)
self.assertEqual(len(tm._synchs), 1)
self.assertTrue(synch in tm._synchs)
def test_unregisterSynch(self):
tm = self._makeOne()
synch1 = DummySynch()
synch2 = DummySynch()
tm.registerSynch(synch1)
tm.registerSynch(synch2)
tm.unregisterSynch(synch1)
self.assertEqual(len(tm._synchs), 1)
self.assertFalse(synch1 in tm._synchs)
self.assertTrue(synch2 in tm._synchs)
def test_isDoomed_wo_existing_txn(self):
tm = self._makeOne()
self.assertFalse(tm.isDoomed())
tm._txn.doom()
self.assertTrue(tm.isDoomed())
def test_isDoomed_w_existing_txn(self):
class Existing(object):
_doomed = False
def isDoomed(self):
return self._doomed
tm = self._makeOne()
tm._txn = txn = Existing()
self.assertFalse(tm.isDoomed())
txn._doomed = True
self.assertTrue(tm.isDoomed())
def test_doom(self):
tm = self._makeOne()
txn = tm.get()
self.assertFalse(txn.isDoomed())
tm.doom()
self.assertTrue(txn.isDoomed())
self.assertTrue(tm.isDoomed())
def test_commit_w_existing_txn(self):
class Existing(object):
_committed = False
def commit(self):
self._committed = True
tm = self._makeOne()
tm._txn = txn = Existing()
tm.commit()
self.assertTrue(txn._committed)
def test_abort_w_existing_txn(self):
class Existing(object):
_aborted = False
def abort(self):
self._aborted = True
tm = self._makeOne()
tm._txn = txn = Existing()
tm.abort()
self.assertTrue(txn._aborted)
def test_as_context_manager_wo_error(self):
class _Test(object):
_committed = False
_aborted = False
def commit(self):
self._committed = True
def abort(self):
self._aborted = True
tm = self._makeOne()
with tm:
tm._txn = txn = _Test()
self.assertTrue(txn._committed)
self.assertFalse(txn._aborted)
def test_as_context_manager_w_error(self):
class _Test(object):
_committed = False
_aborted = False
def commit(self):
self._committed = True
def abort(self):
self._aborted = True
tm = self._makeOne()
try:
with tm:
tm._txn = txn = _Test()
1/0
except ZeroDivisionError:
pass
self.assertFalse(txn._committed)
self.assertTrue(txn._aborted)
def test_savepoint_default(self):
class _Test(object):
_sp = None
def savepoint(self, optimistic):
self._sp = optimistic
tm = self._makeOne()
tm._txn = txn = _Test()
tm.savepoint()
self.assertFalse(txn._sp)
def test_savepoint_explicit(self):
class _Test(object):
_sp = None
def savepoint(self, optimistic):
self._sp = optimistic
tm = self._makeOne()
tm._txn = txn = _Test()
tm.savepoint(True)
self.assertTrue(txn._sp)
def test_attempts_w_invalid_count(self):
tm = self._makeOne()
self.assertRaises(ValueError, list, tm.attempts(0))
self.assertRaises(ValueError, list, tm.attempts(-1))
self.assertRaises(ValueError, list, tm.attempts(-10))
def test_attempts_w_valid_count(self):
tm = self._makeOne()
found = list(tm.attempts(1))
self.assertEqual(len(found), 1)
self.assertTrue(found[0] is tm)
def test_attempts_w_default_count(self):
from transaction._manager import Attempt
tm = self._makeOne()
found = list(tm.attempts())
self.assertEqual(len(found), 3)
for attempt in found[:-1]:
self.assertTrue(isinstance(attempt, Attempt))
self.assertTrue(attempt.manager is tm)
self.assertTrue(found[-1] is tm)
def test__retryable_w_transient_error(self):
from transaction.interfaces import TransientError
tm = self._makeOne()
self.assertTrue(tm._retryable(TransientError, object()))
def test__retryable_w_transient_subclass(self):
from transaction.interfaces import TransientError
class _Derived(TransientError):
pass
tm = self._makeOne()
self.assertTrue(tm._retryable(_Derived, object()))
def test__retryable_w_normal_exception_no_resources(self):
tm = self._makeOne()
self.assertFalse(tm._retryable(Exception, object()))
def test__retryable_w_normal_exception_w_resource_voting_yes(self):
class _Resource(object):
def should_retry(self, err):
return True
tm = self._makeOne()
tm.get()._resources.append(_Resource())
self.assertTrue(tm._retryable(Exception, object()))
# basic tests with two sub trans jars
# really we only need one, so tests for
# sub1 should identical to tests for sub2
def test_commit_normal(self):
mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
sub1.modify()
sub2.modify()
mgr.commit()
assert sub1._p_jar.ccommit_sub == 0
assert sub1._p_jar.ctpc_finish == 1
def test_abort_normal(self):
mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
sub1.modify()
sub2.modify()
mgr.abort()
assert sub2._p_jar.cabort == 1
# repeat adding in a nonsub trans jars
def test_commit_w_nonsub_jar(self):
mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
nosub1.modify()
mgr.commit()
assert nosub1._p_jar.ctpc_finish == 1
def test_abort_w_nonsub_jar(self):
mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
nosub1.modify()
mgr.abort()
assert nosub1._p_jar.ctpc_finish == 0
assert nosub1._p_jar.cabort == 1
### Failure Mode Tests
#
# ok now we do some more interesting
# tests that check the implementations
# error handling by throwing errors from
# various jar methods
###
# first the recoverable errors
def test_abort_w_broken_jar(self):
from transaction import _transaction
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
sub1._p_jar = BasicJar(errors='abort')
nosub1.modify()
sub1.modify(nojar=1)
sub2.modify()
try:
mgr.abort()
except TestTxnException:
pass
assert nosub1._p_jar.cabort == 1
assert sub2._p_jar.cabort == 1
def test_commit_w_broken_jar_commit(self):
from transaction import _transaction
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
sub1._p_jar = BasicJar(errors='commit')
nosub1.modify()
sub1.modify(nojar=1)
try:
mgr.commit()
except TestTxnException:
pass
assert nosub1._p_jar.ctpc_finish == 0
assert nosub1._p_jar.ccommit == 1
assert nosub1._p_jar.ctpc_abort == 1
def test_commit_w_broken_jar_tpc_vote(self):
from transaction import _transaction
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
sub1._p_jar = BasicJar(errors='tpc_vote')
nosub1.modify()
sub1.modify(nojar=1)
try:
mgr.commit()
except TestTxnException:
pass
assert nosub1._p_jar.ctpc_finish == 0
assert nosub1._p_jar.ccommit == 1
assert nosub1._p_jar.ctpc_abort == 1
assert sub1._p_jar.ctpc_abort == 1
def test_commit_w_broken_jar_tpc_begin(self):
# ok this test reveals a bug in the TM.py
# as the nosub tpc_abort there is ignored.
# nosub calling method tpc_begin
# nosub calling method commit
# sub calling method tpc_begin
# sub calling method abort
# sub calling method tpc_abort
# nosub calling method tpc_abort
from transaction import _transaction
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
sub1._p_jar = BasicJar(errors='tpc_begin')
nosub1.modify()
sub1.modify(nojar=1)
try:
mgr.commit()
except TestTxnException:
pass
assert nosub1._p_jar.ctpc_abort == 1
assert sub1._p_jar.ctpc_abort == 1
def test_commit_w_broken_jar_tpc_abort_tpc_vote(self):
from transaction import _transaction
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
sub1._p_jar = BasicJar(errors=('tpc_abort', 'tpc_vote'))
nosub1.modify()
sub1.modify(nojar=1)
try:
mgr.commit()
except TestTxnException:
pass
assert nosub1._p_jar.ctpc_abort == 1
class AttemptTests(unittest.TestCase):
def _makeOne(self, manager):
from transaction._manager import Attempt
return Attempt(manager)
def test___enter__(self):
manager = DummyManager()
inst = self._makeOne(manager)
inst.__enter__()
self.assertTrue(manager.entered)
def test___exit__no_exc_no_commit_exception(self):
manager = DummyManager()
inst = self._makeOne(manager)
result = inst.__exit__(None, None, None)
self.assertFalse(result)
self.assertTrue(manager.committed)
def test___exit__no_exc_nonretryable_commit_exception(self):
manager = DummyManager(raise_on_commit=ValueError)
inst = self._makeOne(manager)
self.assertRaises(ValueError, inst.__exit__, None, None, None)
self.assertTrue(manager.committed)
self.assertTrue(manager.aborted)
def test___exit__no_exc_abort_exception_after_nonretryable_commit_exc(self):
manager = DummyManager(raise_on_abort=ValueError,
raise_on_commit=KeyError)
inst = self._makeOne(manager)
self.assertRaises(ValueError, inst.__exit__, None, None, None)
self.assertTrue(manager.committed)
self.assertTrue(manager.aborted)
def test___exit__no_exc_retryable_commit_exception(self):
from transaction.interfaces import TransientError
manager = DummyManager(raise_on_commit=TransientError)
inst = self._makeOne(manager)
result = inst.__exit__(None, None, None)
self.assertTrue(result)
self.assertTrue(manager.committed)
self.assertTrue(manager.aborted)
def test___exit__with_exception_value_retryable(self):
from transaction.interfaces import TransientError
manager = DummyManager()
inst = self._makeOne(manager)
result = inst.__exit__(TransientError, TransientError(), None)
self.assertTrue(result)
self.assertFalse(manager.committed)
self.assertTrue(manager.aborted)
def test___exit__with_exception_value_nonretryable(self):
manager = DummyManager()
inst = self._makeOne(manager)
self.assertRaises(KeyError, inst.__exit__, KeyError, KeyError(), None)
self.assertFalse(manager.committed)
self.assertTrue(manager.aborted)
class DummyManager(object):
entered = False
committed = False
aborted = False
def __init__(self, raise_on_commit=None, raise_on_abort=None):
self.raise_on_commit = raise_on_commit
self.raise_on_abort = raise_on_abort
def _retryable(self, t, v):
from transaction._manager import TransientError
return issubclass(t, TransientError)
def __enter__(self):
self.entered = True
def abort(self):
self.aborted = True
if self.raise_on_abort:
raise self.raise_on_abort
def commit(self):
self.committed = True
if self.raise_on_commit:
raise self.raise_on_commit
class DataObject:
def __init__(self, transaction_manager, nost=0):
self.transaction_manager = transaction_manager
self.nost = nost
self._p_jar = None
def modify(self, nojar=0, tracing=0):
if not nojar:
if self.nost:
self._p_jar = BasicJar(tracing=tracing)
else:
self._p_jar = BasicJar(tracing=tracing)
self.transaction_manager.get().join(self._p_jar)
class TestTxnException(Exception):
pass
class BasicJar:
def __init__(self, errors=(), tracing=0):
if not isinstance(errors, tuple):
errors = errors,
self.errors = errors
self.tracing = tracing
self.cabort = 0
self.ccommit = 0
self.ctpc_begin = 0
self.ctpc_abort = 0
self.ctpc_vote = 0
self.ctpc_finish = 0
self.cabort_sub = 0
self.ccommit_sub = 0
def __repr__(self):
return "<%s %X %s>" % (self.__class__.__name__,
positive_id(self),
self.errors)
def sortKey(self):
# All these jars use the same sort key, and Python's list.sort()
# is stable. These two
return self.__class__.__name__
def check(self, method):
if self.tracing:
print('%s calling method %s'%(str(self.tracing),method))
if method in self.errors:
raise TestTxnException("error %s" % method)
## basic jar txn interface
def abort(self, *args):
self.check('abort')
self.cabort += 1
def commit(self, *args):
self.check('commit')
self.ccommit += 1
def tpc_begin(self, txn, sub=0):
self.check('tpc_begin')
self.ctpc_begin += 1
def tpc_vote(self, *args):
self.check('tpc_vote')
self.ctpc_vote += 1
def tpc_abort(self, *args):
self.check('tpc_abort')
self.ctpc_abort += 1
def tpc_finish(self, *args):
self.check('tpc_finish')
self.ctpc_finish += 1
class DummySynch(object):
def __init__(self):
self._txns = set()
def newTransaction(self, txn):
self._txns.add(txn)
def positive_id(obj):
"""Return id(obj) as a non-negative integer."""
import struct
_ADDRESS_MASK = 256 ** struct.calcsize('P')
result = id(obj)
if result < 0:
result += _ADDRESS_MASK
assert result > 0
return result
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TransactionManagerTests),
unittest.makeSuite(AttemptTests),
))
##############################################################################
#
# Copyright (c) 2001, 2002, 2005 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""Test transaction behavior for variety of cases.
I wrote these unittests to investigate some odd transaction
behavior when doing unittests of integrating non sub transaction
aware objects, and to insure proper txn behavior. these
tests test the transaction system independent of the rest of the
zodb.
you can see the method calls to a jar by passing the
keyword arg tracing to the modify method of a dataobject.
the value of the arg is a prefix used for tracing print calls
to that objects jar.
the number of times a jar method was called can be inspected
by looking at an attribute of the jar that is the method
name prefixed with a c (count/check).
i've included some tracing examples for tests that i thought
were illuminating as doc strings below.
TODO
add in tests for objects which are modified multiple times,
for example an object that gets modified in multiple sub txns.
"""
import unittest
class TransactionTests(unittest.TestCase):
def _getTargetClass(self):
from transaction._transaction import Transaction
return Transaction
def _makeOne(self, synchronizers=None, manager=None):
return self._getTargetClass()(synchronizers, manager)
def test_ctor_defaults(self):
from transaction.weakset import WeakSet
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
self.assertTrue(isinstance(txn._synchronizers, WeakSet))
self.assertEqual(len(txn._synchronizers), 0)
self.assertTrue(txn._manager is None)
self.assertEqual(txn.user, "")
self.assertEqual(txn.description, "")
self.assertTrue(txn._savepoint2index is None)
self.assertEqual(txn._savepoint_index, 0)
self.assertEqual(txn._resources, [])
self.assertEqual(txn._adapters, {})
self.assertEqual(txn._voted, {})
self.assertEqual(txn._extension, {})
self.assertTrue(txn.log is logger)
self.assertEqual(len(logger._log), 1)
self.assertEqual(logger._log[0][0], 'debug')
self.assertEqual(logger._log[0][1], 'new transaction')
self.assertTrue(txn._failure_traceback is None)
self.assertEqual(txn._before_commit, [])
self.assertEqual(txn._after_commit, [])
def test_ctor_w_syncs(self):
from transaction.weakset import WeakSet
synchs = WeakSet()
txn = self._makeOne(synchronizers=synchs)
self.assertTrue(txn._synchronizers is synchs)
def test_isDoomed(self):
from transaction._transaction import Status
txn = self._makeOne()
self.assertFalse(txn.isDoomed())
txn.status = Status.DOOMED
self.assertTrue(txn.isDoomed())
def test_doom_active(self):
from transaction._transaction import Status
txn = self._makeOne()
txn.doom()
self.assertTrue(txn.isDoomed())
self.assertEqual(txn.status, Status.DOOMED)
def test_doom_invalid(self):
from transaction._transaction import Status
txn = self._makeOne()
for status in Status.COMMITTING, Status.COMMITTED, Status.COMMITFAILED:
txn.status = status
self.assertRaises(ValueError, txn.doom)
def test_doom_already_doomed(self):
from transaction._transaction import Status
txn = self._makeOne()
txn.status = Status.DOOMED
self.assertTrue(txn.isDoomed())
self.assertEqual(txn.status, Status.DOOMED)
def test__prior_operation_failed(self):
from transaction.interfaces import TransactionFailedError
from transaction.tests.common import assertRaisesEx
class _Traceback(object):
def getvalue(self):
return 'TRACEBACK'
txn = self._makeOne()
txn._failure_traceback = _Traceback()
err = assertRaisesEx(TransactionFailedError,
txn._prior_operation_failed)
self.assertTrue(str(err).startswith('An operation previously failed'))
self.assertTrue(str(err).endswith( "with traceback:\n\nTRACEBACK"))
def test_join_COMMITFAILED(self):
from transaction.interfaces import TransactionFailedError
from transaction._transaction import Status
class _Traceback(object):
def getvalue(self):
return 'TRACEBACK'
txn = self._makeOne()
txn.status = Status.COMMITFAILED
txn._failure_traceback = _Traceback()
self.assertRaises(TransactionFailedError, txn.join, object())
def test_join_COMMITTING(self):
from transaction._transaction import Status
txn = self._makeOne()
txn.status = Status.COMMITTING
self.assertRaises(ValueError, txn.join, object())
def test_join_COMMITTED(self):
from transaction._transaction import Status
txn = self._makeOne()
txn.status = Status.COMMITTED
self.assertRaises(ValueError, txn.join, object())
def test_join_DOOMED_non_preparing_wo_sp2index(self):
from transaction._transaction import Status
txn = self._makeOne()
txn.status = Status.DOOMED
resource = object()
txn.join(resource)
self.assertEqual(txn._resources, [resource])
def test_join_ACTIVE_w_preparing_w_sp2index(self):
from transaction._transaction import AbortSavepoint
from transaction._transaction import DataManagerAdapter
class _TSP(object):
def __init__(self):
self._savepoints = []
class _DM(object):
def prepare(self):
pass
txn = self._makeOne()
tsp = _TSP()
txn._savepoint2index = {tsp: object()}
dm = _DM
txn.join(dm)
self.assertEqual(len(txn._resources), 1)
dma = txn._resources[0]
self.assertTrue(isinstance(dma, DataManagerAdapter))
self.assertTrue(txn._resources[0]._datamanager is dm)
self.assertEqual(len(tsp._savepoints), 1)
self.assertTrue(isinstance(tsp._savepoints[0], AbortSavepoint))
self.assertTrue(tsp._savepoints[0].datamanager is dma)
self.assertTrue(tsp._savepoints[0].transaction is txn)
def test__unjoin_miss(self):
txn = self._makeOne()
txn._unjoin(object()) #no raise
def test__unjoin_hit(self):
txn = self._makeOne()
resource = object()
txn._resources.append(resource)
txn._unjoin(resource)
self.assertEqual(txn._resources, [])
def test_savepoint_COMMITFAILED(self):
from transaction.interfaces import TransactionFailedError
from transaction._transaction import Status
class _Traceback(object):
def getvalue(self):
return 'TRACEBACK'
txn = self._makeOne()
txn.status = Status.COMMITFAILED
txn._failure_traceback = _Traceback()
self.assertRaises(TransactionFailedError, txn.savepoint)
def test_savepoint_empty(self):
from weakref import WeakKeyDictionary
from transaction import _transaction
from transaction._transaction import Savepoint
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
sp = txn.savepoint()
self.assertTrue(isinstance(sp, Savepoint))
self.assertTrue(sp.transaction is txn)
self.assertEqual(sp._savepoints, [])
self.assertEqual(txn._savepoint_index, 1)
self.assertTrue(isinstance(txn._savepoint2index, WeakKeyDictionary))
self.assertEqual(txn._savepoint2index[sp], 1)
def test_savepoint_non_optimistc_resource_wo_support(self):
from transaction import _transaction
from transaction._transaction import Status
from transaction._compat import StringIO
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
resource = object()
txn._resources.append(resource)
self.assertRaises(TypeError, txn.savepoint)
self.assertEqual(txn.status, Status.COMMITFAILED)
self.assertTrue(isinstance(txn._failure_traceback, StringIO))
self.assertTrue('TypeError' in txn._failure_traceback.getvalue())
self.assertEqual(len(logger._log), 2)
self.assertEqual(logger._log[0][0], 'error')
self.assertTrue(logger._log[0][1].startswith('Error in abort'))
self.assertEqual(logger._log[1][0], 'error')
self.assertTrue(logger._log[1][1].startswith('Error in tpc_abort'))
def test__remove_and_invalidate_after_miss(self):
from weakref import WeakKeyDictionary
txn = self._makeOne()
txn._savepoint2index = WeakKeyDictionary()
class _SP(object):
def __init__(self, txn):
self.transaction = txn
holdme = []
for i in range(10):
sp = _SP(txn)
holdme.append(sp) #prevent gc
txn._savepoint2index[sp] = i
self.assertEqual(len(txn._savepoint2index), 10)
self.assertRaises(KeyError, txn._remove_and_invalidate_after, _SP(txn))
self.assertEqual(len(txn._savepoint2index), 10)
def test__remove_and_invalidate_after_hit(self):
from weakref import WeakKeyDictionary
txn = self._makeOne()
txn._savepoint2index = WeakKeyDictionary()
class _SP(object):
def __init__(self, txn, index):
self.transaction = txn
self._index = index
def __lt__(self, other):
return self._index < other._index
def __repr__(self):
return '_SP: %d' % self._index
holdme = []
for i in range(10):
sp = _SP(txn, i)
holdme.append(sp) #prevent gc
txn._savepoint2index[sp] = i
self.assertEqual(len(txn._savepoint2index), 10)
txn._remove_and_invalidate_after(holdme[1])
self.assertEqual(sorted(txn._savepoint2index), sorted(holdme[:2]))
def test__invalidate_all_savepoints(self):
from weakref import WeakKeyDictionary
txn = self._makeOne()
txn._savepoint2index = WeakKeyDictionary()
class _SP(object):
def __init__(self, txn, index):
self.transaction = txn
self._index = index
def __repr__(self):
return '_SP: %d' % self._index
holdme = []
for i in range(10):
sp = _SP(txn, i)
holdme.append(sp) #prevent gc
txn._savepoint2index[sp] = i
self.assertEqual(len(txn._savepoint2index), 10)
txn._invalidate_all_savepoints()
self.assertEqual(list(txn._savepoint2index), [])
def test_register_wo_jar(self):
class _Dummy(object):
_p_jar = None
txn = self._makeOne()
self.assertRaises(ValueError, txn.register, _Dummy())
def test_register_w_jar(self):
class _Manager(object):
pass
mgr = _Manager()
class _Dummy(object):
_p_jar = mgr
txn = self._makeOne()
dummy = _Dummy()
txn.register(dummy)
resources = list(txn._resources)
self.assertEqual(len(resources), 1)
adapter = resources[0]
self.assertTrue(adapter.manager is mgr)
self.assertTrue(dummy in adapter.objects)
items = list(txn._adapters.items())
self.assertEqual(len(items), 1)
self.assertTrue(items[0][0] is mgr)
self.assertTrue(items[0][1] is adapter)
def test_register_w_jar_already_adapted(self):
class _Adapter(object):
def __init__(self):
self.objects = []
class _Manager(object):
pass
mgr = _Manager()
class _Dummy(object):
_p_jar = mgr
txn = self._makeOne()
txn._adapters[mgr] = adapter = _Adapter()
dummy = _Dummy()
txn.register(dummy)
self.assertTrue(dummy in adapter.objects)
def test_commit_DOOMED(self):
from transaction.interfaces import DoomedTransaction
from transaction._transaction import Status
txn = self._makeOne()
txn.status = Status.DOOMED
self.assertRaises(DoomedTransaction, txn.commit)
def test_commit_COMMITFAILED(self):
from transaction._transaction import Status
from transaction.interfaces import TransactionFailedError
class _Traceback(object):
def getvalue(self):
return 'TRACEBACK'
txn = self._makeOne()
txn.status = Status.COMMITFAILED
txn._failure_traceback = _Traceback()
self.assertRaises(TransactionFailedError, txn.commit)
def test_commit_wo_savepoints_wo_hooks_wo_synchronizers(self):
from transaction._transaction import Status
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
class _Mgr(object):
def __init__(self, txn):
self._txn = txn
def free(self, txn):
assert txn is self._txn
self._txn = None
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
mgr = txn._manager = _Mgr(txn)
txn.commit()
self.assertEqual(txn.status, Status.COMMITTED)
self.assertTrue(mgr._txn is None)
self.assertEqual(logger._log[0][0], 'debug')
self.assertEqual(logger._log[0][1], 'commit')
def test_commit_w_savepoints(self):
from weakref import WeakKeyDictionary
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
class _SP(object):
def __init__(self, txn, index):
self.transaction = txn
self._index = index
def __repr__(self):
return '_SP: %d' % self._index
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
txn._savepoint2index = WeakKeyDictionary()
holdme = []
for i in range(10):
sp = _SP(txn, i)
holdme.append(sp) #prevent gc
txn._savepoint2index[sp] = i
logger._clear()
txn.commit()
self.assertEqual(list(txn._savepoint2index), [])
def test_commit_w_beforeCommitHooks(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
_hooked1, _hooked2 = [], []
def _hook1(*args, **kw):
_hooked1.append((args, kw))
def _hook2(*args, **kw):
_hooked2.append((args, kw))
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
txn._before_commit.append((_hook1, ('one',), {'uno': 1}))
txn._before_commit.append((_hook2, (), {}))
logger._clear()
txn.commit()
self.assertEqual(_hooked1, [(('one',), {'uno': 1})])
self.assertEqual(_hooked2, [((), {})])
self.assertEqual(txn._before_commit, [])
def test_commit_w_synchronizers(self):
from transaction.weakset import WeakSet
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
class _Synch(object):
_before = _after = False
def beforeCompletion(self, txn):
self._before = txn
def afterCompletion(self, txn):
self._after = txn
synchs = [_Synch(), _Synch(), _Synch()]
ws = WeakSet()
for synch in synchs:
ws.add(synch)
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne(synchronizers=ws)
logger._clear()
txn.commit()
for synch in synchs:
self.assertTrue(synch._before is txn)
self.assertTrue(synch._after is txn)
def test_commit_w_afterCommitHooks(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
_hooked1, _hooked2 = [], []
def _hook1(*args, **kw):
_hooked1.append((args, kw))
def _hook2(*args, **kw):
_hooked2.append((args, kw))
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
txn._after_commit.append((_hook2, (), {}))
logger._clear()
txn.commit()
self.assertEqual(_hooked1, [((True, 'one',), {'uno': 1})])
self.assertEqual(_hooked2, [((True,), {})])
self.assertEqual(txn._after_commit, [])
def test_commit_error_w_afterCompleteHooks(self):
from transaction import _transaction
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
class BrokenResource(object):
def sortKey(self):
return 'zzz'
def tpc_begin(self, txn):
raise ValueError('test')
broken = BrokenResource()
resource = Resource('aaa')
_hooked1, _hooked2 = [], []
def _hook1(*args, **kw):
_hooked1.append((args, kw))
def _hook2(*args, **kw):
_hooked2.append((args, kw))
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
txn._after_commit.append((_hook2, (), {}))
txn._resources.append(broken)
txn._resources.append(resource)
logger._clear()
self.assertRaises(ValueError, txn.commit)
self.assertEqual(_hooked1, [((False, 'one',), {'uno': 1})])
self.assertEqual(_hooked2, [((False,), {})])
self.assertEqual(txn._after_commit, [])
self.assertTrue(resource._b)
self.assertFalse(resource._c)
self.assertFalse(resource._v)
self.assertFalse(resource._f)
self.assertTrue(resource._a)
self.assertTrue(resource._x)
def test_commit_error_w_synchronizers(self):
from transaction.weakset import WeakSet
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
class _Synch(object):
_before = _after = False
def beforeCompletion(self, txn):
self._before = txn
def afterCompletion(self, txn):
self._after = txn
synchs = [_Synch(), _Synch(), _Synch()]
ws = WeakSet()
for synch in synchs:
ws.add(synch)
class BrokenResource(object):
def sortKey(self):
return 'zzz'
def tpc_begin(self, txn):
raise ValueError('test')
broken = BrokenResource()
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne(synchronizers=ws)
logger._clear()
txn._resources.append(broken)
self.assertRaises(ValueError, txn.commit)
for synch in synchs:
self.assertTrue(synch._before is txn)
self.assertTrue(synch._after is txn) #called in _cleanup
def test_getBeforeCommitHooks_empty(self):
txn = self._makeOne()
self.assertEqual(list(txn.getBeforeCommitHooks()), [])
def test_addBeforeCommitHook(self):
def _hook(*args, **kw):
pass
txn = self._makeOne()
txn.addBeforeCommitHook(_hook, ('one',), dict(uno=1))
self.assertEqual(list(txn.getBeforeCommitHooks()),
[(_hook, ('one',), {'uno': 1})])
def test_addBeforeCommitHook_w_kws(self):
def _hook(*args, **kw):
pass
txn = self._makeOne()
txn.addBeforeCommitHook(_hook, ('one',))
self.assertEqual(list(txn.getBeforeCommitHooks()),
[(_hook, ('one',), {})])
def test_getAfterCommitHooks_empty(self):
txn = self._makeOne()
self.assertEqual(list(txn.getAfterCommitHooks()), [])
def test_addAfterCommitHook(self):
def _hook(*args, **kw):
pass
txn = self._makeOne()
txn.addAfterCommitHook(_hook, ('one',), dict(uno=1))
self.assertEqual(list(txn.getAfterCommitHooks()),
[(_hook, ('one',), {'uno': 1})])
def test_addAfterCommitHook_wo_kws(self):
def _hook(*args, **kw):
pass
txn = self._makeOne()
txn.addAfterCommitHook(_hook, ('one',))
self.assertEqual(list(txn.getAfterCommitHooks()),
[(_hook, ('one',), {})])
def test_callAfterCommitHook_w_error(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
_hooked2 = []
def _hook1(*args, **kw):
raise ValueError()
def _hook2(*args, **kw):
_hooked2.append((args, kw))
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
txn.addAfterCommitHook(_hook1, ('one',))
txn.addAfterCommitHook(_hook2, ('two',), dict(dos=2))
txn._callAfterCommitHooks()
# second hook gets called even if first raises
self.assertEqual(_hooked2, [((True, 'two',), {'dos': 2})])
self.assertEqual(len(logger._log), 1)
self.assertEqual(logger._log[0][0], 'error')
self.assertTrue(logger._log[0][1].startswith(
"Error in after commit hook"))
def test_callAfterCommitHook_w_abort(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
_hooked2 = []
def _hook1(*args, **kw):
raise ValueError()
def _hook2(*args, **kw):
_hooked2.append((args, kw))
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
txn.addAfterCommitHook(_hook1, ('one',))
txn.addAfterCommitHook(_hook2, ('two',), dict(dos=2))
txn._callAfterCommitHooks()
self.assertEqual(logger._log[0][0], 'error')
self.assertTrue(logger._log[0][1].startswith(
"Error in after commit hook"))
def test__commitResources_normal(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
resources = [Resource('bbb'), Resource('aaa')]
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
txn._resources.extend(resources)
txn._commitResources()
self.assertEqual(len(txn._voted), 2)
for r in resources:
self.assertTrue(r._b and r._c and r._v and r._f)
self.assertFalse(r._a and r._x)
self.assertTrue(id(r) in txn._voted)
self.assertEqual(len(logger._log), 2)
self.assertEqual(logger._log[0][0], 'debug')
self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
self.assertEqual(logger._log[1][0], 'debug')
self.assertEqual(logger._log[1][1], 'commit Resource: bbb')
def test__commitResources_error_in_tpc_begin(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
resources = [Resource('bbb', 'tpc_begin'), Resource('aaa')]
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
txn._resources.extend(resources)
self.assertRaises(ValueError, txn._commitResources)
for r in resources:
if r._key == 'aaa':
self.assertTrue(r._b)
else:
self.assertFalse(r._b)
self.assertFalse(r._c and r._v and r._f)
self.assertTrue(r._a and r._x)
self.assertEqual(len(logger._log), 0)
def test__commitResources_error_in_commit(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
resources = [Resource('bbb', 'commit'), Resource('aaa')]
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
txn._resources.extend(resources)
self.assertRaises(ValueError, txn._commitResources)
for r in resources:
self.assertTrue(r._b)
if r._key == 'aaa':
self.assertTrue(r._c)
else:
self.assertFalse(r._c)
self.assertFalse(r._v and r._f)
self.assertTrue(r._a and r._x)
self.assertEqual(len(logger._log), 1)
self.assertEqual(logger._log[0][0], 'debug')
self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
def test__commitResources_error_in_tpc_vote(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
resources = [Resource('bbb', 'tpc_vote'), Resource('aaa')]
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
txn._resources.extend(resources)
self.assertRaises(ValueError, txn._commitResources)
self.assertEqual(len(txn._voted), 1)
for r in resources:
self.assertTrue(r._b and r._c)
if r._key == 'aaa':
self.assertTrue(id(r) in txn._voted)
self.assertTrue(r._v)
self.assertFalse(r._f)
self.assertFalse(r._a)
self.assertTrue(r._x)
else:
self.assertFalse(id(r) in txn._voted)
self.assertFalse(r._v)
self.assertFalse(r._f)
self.assertTrue(r._a and r._x)
self.assertEqual(len(logger._log), 2)
self.assertEqual(logger._log[0][0], 'debug')
self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
self.assertEqual(logger._log[1][0], 'debug')
self.assertEqual(logger._log[1][1], 'commit Resource: bbb')
def test__commitResources_error_in_tpc_finish(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
resources = [Resource('bbb', 'tpc_finish'), Resource('aaa')]
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
txn._resources.extend(resources)
self.assertRaises(ValueError, txn._commitResources)
for r in resources:
self.assertTrue(r._b and r._c and r._v)
self.assertTrue(id(r) in txn._voted)
if r._key == 'aaa':
self.assertTrue(r._f)
else:
self.assertFalse(r._f)
self.assertFalse(r._a and r._x) #no cleanup if tpc_finish raises
self.assertEqual(len(logger._log), 3)
self.assertEqual(logger._log[0][0], 'debug')
self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
self.assertEqual(logger._log[1][0], 'debug')
self.assertEqual(logger._log[1][1], 'commit Resource: bbb')
self.assertEqual(logger._log[2][0], 'critical')
self.assertTrue(logger._log[2][1].startswith(
'A storage error occurred'))
def test_abort_wo_savepoints_wo_hooks_wo_synchronizers(self):
from transaction._transaction import Status
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
class _Mgr(object):
def __init__(self, txn):
self._txn = txn
def free(self, txn):
assert txn is self._txn
self._txn = None
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
logger._clear()
mgr = txn._manager = _Mgr(txn)
txn.abort()
self.assertEqual(txn.status, Status.ACTIVE)
self.assertTrue(mgr._txn is None)
self.assertEqual(logger._log[0][0], 'debug')
self.assertEqual(logger._log[0][1], 'abort')
def test_abort_w_savepoints(self):
from weakref import WeakKeyDictionary
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
class _SP(object):
def __init__(self, txn, index):
self.transaction = txn
self._index = index
def __repr__(self):
return '_SP: %d' % self._index
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
txn._savepoint2index = WeakKeyDictionary()
holdme = []
for i in range(10):
sp = _SP(txn, i)
holdme.append(sp) #prevent gc
txn._savepoint2index[sp] = i
logger._clear()
txn.abort()
self.assertEqual(list(txn._savepoint2index), [])
def test_abort_w_beforeCommitHooks(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
_hooked1, _hooked2 = [], []
def _hook1(*args, **kw):
_hooked1.append((args, kw))
def _hook2(*args, **kw):
_hooked2.append((args, kw))
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
txn._before_commit.append((_hook1, ('one',), {'uno': 1}))
txn._before_commit.append((_hook2, (), {}))
logger._clear()
txn.abort()
self.assertEqual(_hooked1, [])
self.assertEqual(_hooked2, [])
# Hooks are neither called nor cleared on abort
self.assertEqual(list(txn.getBeforeCommitHooks()),
[(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
def test_abort_w_synchronizers(self):
from transaction.weakset import WeakSet
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
class _Synch(object):
_before = _after = False
def beforeCompletion(self, txn):
self._before = txn
def afterCompletion(self, txn):
self._after = txn
synchs = [_Synch(), _Synch(), _Synch()]
ws = WeakSet()
for synch in synchs:
ws.add(synch)
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne(synchronizers=ws)
logger._clear()
txn.abort()
for synch in synchs:
self.assertTrue(synch._before is txn)
self.assertTrue(synch._after is txn)
def test_abort_w_afterCommitHooks(self):
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
_hooked1, _hooked2 = [], []
def _hook1(*args, **kw):
_hooked1.append((args, kw))
def _hook2(*args, **kw):
_hooked2.append((args, kw))
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
txn._after_commit.append((_hook2, (), {}))
logger._clear()
txn.abort()
# Hooks are neither called nor cleared on abort
self.assertEqual(_hooked1, [])
self.assertEqual(_hooked2, [])
self.assertEqual(list(txn.getAfterCommitHooks()),
[(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
def test_abort_error_w_afterCompleteHooks(self):
from transaction import _transaction
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
class BrokenResource(object):
def sortKey(self):
return 'zzz'
def abort(self, txn):
raise ValueError('test')
broken = BrokenResource()
resource = Resource('aaa')
_hooked1, _hooked2 = [], []
def _hook1(*args, **kw):
_hooked1.append((args, kw))
def _hook2(*args, **kw):
_hooked2.append((args, kw))
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
txn = self._makeOne()
txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
txn._after_commit.append((_hook2, (), {}))
txn._resources.append(broken)
txn._resources.append(resource)
logger._clear()
self.assertRaises(ValueError, txn.abort)
# Hooks are neither called nor cleared on abort
self.assertEqual(_hooked1, [])
self.assertEqual(_hooked2, [])
self.assertEqual(list(txn.getAfterCommitHooks()),
[(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
self.assertTrue(resource._a)
self.assertFalse(resource._x)
def test_abort_error_w_synchronizers(self):
from transaction.weakset import WeakSet
from transaction.tests.common import DummyLogger
from transaction.tests.common import Monkey
from transaction import _transaction
class _Synch(object):
_before = _after = False
def beforeCompletion(self, txn):
self._before = txn
def afterCompletion(self, txn):
self._after = txn
synchs = [_Synch(), _Synch(), _Synch()]
ws = WeakSet()
for synch in synchs:
ws.add(synch)
class BrokenResource(object):
def sortKey(self):
return 'zzz'
def abort(self, txn):
raise ValueError('test')
broken = BrokenResource()
logger = DummyLogger()
with Monkey(_transaction, _LOGGER=logger):
t = self._makeOne(synchronizers=ws)
logger._clear()
t._resources.append(broken)
self.assertRaises(ValueError, t.abort)
for synch in synchs:
self.assertTrue(synch._before is t)
self.assertTrue(synch._after is t) #called in _cleanup
def test_note(self):
txn = self._makeOne()
try:
txn.note('This is a note.')
self.assertEqual(txn.description, 'This is a note.')
txn.note('Another.')
self.assertEqual(txn.description, 'This is a note.\nAnother.')
finally:
txn.abort()
def test_setUser_default_path(self):
txn = self._makeOne()
txn.setUser('phreddy')
self.assertEqual(txn.user, '/ phreddy')
def test_setUser_explicit_path(self):
txn = self._makeOne()
txn.setUser('phreddy', '/bedrock')
self.assertEqual(txn.user, '/bedrock phreddy')
def test_setExtendedInfo_single(self):
txn = self._makeOne()
txn.setExtendedInfo('frob', 'qux')
self.assertEqual(txn._extension, {'frob': 'qux'})
def test_setExtendedInfo_multiple(self):
txn = self._makeOne()
txn.setExtendedInfo('frob', 'qux')
txn.setExtendedInfo('baz', 'spam')
txn.setExtendedInfo('frob', 'quxxxx')
self.assertEqual(txn._extension, {'frob': 'quxxxx', 'baz': 'spam'})
class MultiObjectResourceAdapterTests(unittest.TestCase):
def _getTargetClass(self):
from transaction._transaction import MultiObjectResourceAdapter
return MultiObjectResourceAdapter
def _makeOne(self, jar):
return self._getTargetClass()(jar)
def _makeJar(self, key):
class _Resource(Resource):
def __init__(self, key):
super(_Resource, self).__init__(key)
self._c = []
self._a = []
def commit(self, obj, txn):
self._c.append((obj, txn))
def abort(self, obj, txn):
self._a.append((obj, txn))
return _Resource(key)
def _makeDummy(self, kind, name):
class _Dummy(object):
def __init__(self, kind, name):
self._kind = kind
self._name = name
def __repr__(self):
return '<%s: %s>' % (self._kind, self._name)
return _Dummy(kind, name)
def test_ctor(self):
jar = self._makeJar('aaa')
mora = self._makeOne(jar)
self.assertTrue(mora.manager is jar)
self.assertEqual(mora.objects, [])
self.assertEqual(mora.ncommitted, 0)
def test___repr__(self):
jar = self._makeJar('bbb')
mora = self._makeOne(jar)
self.assertEqual(repr(mora),
'<MultiObjectResourceAdapter '
'for Resource: bbb at %s>' % id(mora))
def test_sortKey(self):
jar = self._makeJar('ccc')
mora = self._makeOne(jar)
self.assertEqual(mora.sortKey(), 'ccc')
def test_tpc_begin(self):
jar = self._makeJar('ddd')
mora = self._makeOne(jar)
txn = object()
mora.tpc_begin(txn)
self.assertTrue(jar._b)
def test_commit(self):
jar = self._makeJar('eee')
objects = [self._makeDummy('obj', 'a'), self._makeDummy('obj', 'b')]
mora = self._makeOne(jar)
mora.objects.extend(objects)
txn = self._makeDummy('txn', 'c')
mora.commit(txn)
self.assertEqual(jar._c, [(objects[0], txn), (objects[1], txn)])
def test_tpc_vote(self):
jar = self._makeJar('fff')
mora = self._makeOne(jar)
txn = object()
mora.tpc_vote(txn)
self.assertTrue(jar._v)
def test_tpc_finish(self):
jar = self._makeJar('ggg')
mora = self._makeOne(jar)
txn = object()
mora.tpc_finish(txn)
self.assertTrue(jar._f)
def test_abort(self):
jar = self._makeJar('hhh')
objects = [self._makeDummy('obj', 'a'), self._makeDummy('obj', 'b')]
mora = self._makeOne(jar)
mora.objects.extend(objects)
txn = self._makeDummy('txn', 'c')
mora.abort(txn)
self.assertEqual(jar._a, [(objects[0], txn), (objects[1], txn)])
def test_abort_w_error(self):
from transaction.tests.common import DummyLogger
jar = self._makeJar('hhh')
objects = [self._makeDummy('obj', 'a'),
self._makeDummy('obj', 'b'),
self._makeDummy('obj', 'c'),
]
_old_abort = jar.abort
def _abort(obj, txn):
if obj._name == 'b':
raise ValueError()
_old_abort(obj, txn)
jar.abort = _abort
mora = self._makeOne(jar)
mora.objects.extend(objects)
txn = self._makeDummy('txn', 'c')
txn.log = log = DummyLogger()
self.assertRaises(ValueError, mora.abort, txn)
self.assertEqual(jar._a, [(objects[0], txn), (objects[2], txn)])
def test_tpc_abort(self):
jar = self._makeJar('iii')
mora = self._makeOne(jar)
txn = object()
mora.tpc_abort(txn)
self.assertTrue(jar._x)
class Test_rm_key(unittest.TestCase):
def _callFUT(self, oid):
from transaction._transaction import rm_key
return rm_key(oid)
def test_miss(self):
self.assertTrue(self._callFUT(object()) is None)
def test_hit(self):
self.assertEqual(self._callFUT(Resource('zzz')), 'zzz')
class Test_object_hint(unittest.TestCase):
def _callFUT(self, oid):
from transaction._transaction import object_hint
return object_hint(oid)
def test_miss(self):
class _Test(object):
pass
test = _Test()
self.assertEqual(self._callFUT(test), "_Test oid=None")
def test_hit(self):
class _Test(object):
pass
test = _Test()
test._p_oid = 'OID'
self.assertEqual(self._callFUT(test), "_Test oid='OID'")
class Test_oid_repr(unittest.TestCase):
def _callFUT(self, oid):
from transaction._transaction import oid_repr
return oid_repr(oid)
def test_as_nonstring(self):
self.assertEqual(self._callFUT(123), '123')
def test_as_string_not_8_chars(self):
self.assertEqual(self._callFUT('a'), "'a'")
def test_as_string_z64(self):
s = '\0'*8
self.assertEqual(self._callFUT(s), '0x00')
def test_as_string_all_Fs(self):
s = '\1'*8
self.assertEqual(self._callFUT(s), '0x0101010101010101')
class DataManagerAdapterTests(unittest.TestCase):
def _getTargetClass(self):
from transaction._transaction import DataManagerAdapter
return DataManagerAdapter
def _makeOne(self, jar):
return self._getTargetClass()(jar)
def _makeJar(self, key):
class _Resource(Resource):
_p = False
def prepare(self, txn):
self._p = True
return _Resource(key)
def _makeDummy(self, kind, name):
class _Dummy(object):
def __init__(self, kind, name):
self._kind = kind
self._name = name
def __repr__(self):
return '<%s: %s>' % (self._kind, self._name)
return _Dummy(kind, name)
def test_ctor(self):
jar = self._makeJar('aaa')
dma = self._makeOne(jar)
self.assertTrue(dma._datamanager is jar)
def test_commit(self):
jar = self._makeJar('bbb')
mora = self._makeOne(jar)
txn = self._makeDummy('txn', 'c')
mora.commit(txn)
self.assertFalse(jar._c) #no-op
def test_abort(self):
jar = self._makeJar('ccc')
mora = self._makeOne(jar)
txn = self._makeDummy('txn', 'c')
mora.abort(txn)
self.assertTrue(jar._a)
def test_tpc_begin(self):
jar = self._makeJar('ddd')
mora = self._makeOne(jar)
txn = object()
mora.tpc_begin(txn)
self.assertFalse(jar._b) #no-op
def test_tpc_abort(self):
jar = self._makeJar('eee')
mora = self._makeOne(jar)
txn = object()
mora.tpc_abort(txn)
self.assertFalse(jar._f)
self.assertTrue(jar._a)
def test_tpc_finish(self):
jar = self._makeJar('fff')
mora = self._makeOne(jar)
txn = object()
mora.tpc_finish(txn)
self.assertFalse(jar._f)
self.assertTrue(jar._c)
def test_tpc_vote(self):
jar = self._makeJar('ggg')
mora = self._makeOne(jar)
txn = object()
mora.tpc_vote(txn)
self.assertFalse(jar._v)
self.assertTrue(jar._p)
def test_sortKey(self):
jar = self._makeJar('hhh')
mora = self._makeOne(jar)
self.assertEqual(mora.sortKey(), 'hhh')
class SavepointTests(unittest.TestCase):
def _getTargetClass(self):
from transaction._transaction import Savepoint
return Savepoint
def _makeOne(self, txn, optimistic, *resources):
return self._getTargetClass()(txn, optimistic, *resources)
def test_ctor_w_savepoint_oblivious_resource_non_optimistic(self):
txn = object()
resource = object()
self.assertRaises(TypeError, self._makeOne, txn, False, resource)
def test_ctor_w_savepoint_oblivious_resource_optimistic(self):
from transaction._transaction import NoRollbackSavepoint
txn = object()
resource = object()
sp = self._makeOne(txn, True, resource)
self.assertEqual(len(sp._savepoints), 1)
self.assertTrue(isinstance(sp._savepoints[0], NoRollbackSavepoint))
self.assertTrue(sp._savepoints[0].datamanager is resource)
def test_ctor_w_savepoint_aware_resources(self):
class _Aware(object):
def savepoint(self):
return self
txn = object()
one = _Aware()
another = _Aware()
sp = self._makeOne(txn, True, one, another)
self.assertEqual(len(sp._savepoints), 2)
self.assertTrue(isinstance(sp._savepoints[0], _Aware))
self.assertTrue(sp._savepoints[0] is one)
self.assertTrue(isinstance(sp._savepoints[1], _Aware))
self.assertTrue(sp._savepoints[1] is another)
def test_rollback_w_txn_None(self):
from transaction.interfaces import InvalidSavepointRollbackError
txn = None
class _Aware(object):
def savepoint(self):
return self
resource = _Aware()
sp = self._makeOne(txn, False, resource)
self.assertRaises(InvalidSavepointRollbackError, sp.rollback)
def test_rollback_w_sp_error(self):
class _TXN(object):
_sarce = False
_raia = None
def _saveAndRaiseCommitishError(self):
import sys
from transaction._compat import reraise
self._sarce = True
reraise(*sys.exc_info())
def _remove_and_invalidate_after(self, sp):
self._raia = sp
class _Broken(object):
def rollback(self):
raise ValueError()
_broken = _Broken()
class _GonnaRaise(object):
def savepoint(self):
return _broken
txn = _TXN()
resource = _GonnaRaise()
sp = self._makeOne(txn, False, resource)
self.assertRaises(ValueError, sp.rollback)
self.assertTrue(txn._raia is sp)
self.assertTrue(txn._sarce)
class AbortSavepointTests(unittest.TestCase):
def _getTargetClass(self):
from transaction._transaction import AbortSavepoint
return AbortSavepoint
def _makeOne(self, datamanager, transaction):
return self._getTargetClass()(datamanager, transaction)
def test_ctor(self):
dm = object()
txn = object()
asp = self._makeOne(dm, txn)
self.assertTrue(asp.datamanager is dm)
self.assertTrue(asp.transaction is txn)
def test_rollback(self):
class _DM(object):
_aborted = None
def abort(self, txn):
self._aborted = txn
class _TXN(object):
_unjoined = None
def _unjoin(self, datamanager):
self._unjoin = datamanager
dm = _DM()
txn = _TXN()
asp = self._makeOne(dm, txn)
asp.rollback()
self.assertTrue(dm._aborted is txn)
self.assertTrue(txn._unjoin is dm)
class NoRollbackSavepointTests(unittest.TestCase):
def _getTargetClass(self):
from transaction._transaction import NoRollbackSavepoint
return NoRollbackSavepoint
def _makeOne(self, datamanager):
return self._getTargetClass()(datamanager)
def test_ctor(self):
dm = object()
nrsp = self._makeOne(dm)
self.assertTrue(nrsp.datamanager is dm)
def test_rollback(self):
dm = object()
nrsp = self._makeOne(dm)
self.assertRaises(TypeError, nrsp.rollback)
class MiscellaneousTests(unittest.TestCase):
def test_BBB_join(self):
# The join method is provided for "backward-compatability" with ZODB 4
# data managers.
from transaction import Transaction
from transaction.tests.examples import DataManager
from transaction._transaction import DataManagerAdapter
# The argument to join must be a zodb4 data manager,
# transaction.interfaces.IDataManager.
txn = Transaction()
dm = DataManager()
txn.join(dm)
# The end result is that a data manager adapter is one of the
# transaction's objects:
self.assertTrue(isinstance(txn._resources[0], DataManagerAdapter))
self.assertTrue(txn._resources[0]._datamanager is dm)
def test_bug239086(self):
# The original implementation of thread transaction manager made
# invalid assumptions about thread ids.
import threading
import transaction
import transaction.tests.savepointsample as SPS
dm = SPS.SampleSavepointDataManager()
self.assertEqual(list(dm.keys()), [])
class Sync:
def __init__(self, label):
self.label = label
self.log = []
def beforeCompletion(self, txn):
self.log.append('%s %s' % (self.label, 'before'))
def afterCompletion(self, txn):
self.log.append('%s %s' % (self.label, 'after'))
def newTransaction(self, txn):
self.log.append('%s %s' % (self.label, 'new'))
def run_in_thread(f):
txn = threading.Thread(target=f)
txn.start()
txn.join()
sync = Sync(1)
@run_in_thread
def first():
transaction.manager.registerSynch(sync)
transaction.manager.begin()
dm['a'] = 1
self.assertEqual(sync.log, ['1 new'])
@run_in_thread
def second():
transaction.abort() # should do nothing.
self.assertEqual(sync.log, ['1 new'])
self.assertEqual(list(dm.keys()), ['a'])
dm = SPS.SampleSavepointDataManager()
self.assertEqual(list(dm.keys()), [])
@run_in_thread
def third():
dm['a'] = 1
self.assertEqual(sync.log, ['1 new'])
transaction.abort() # should do nothing
self.assertEqual(list(dm.keys()), ['a'])
class Resource(object):
_b = _c = _v = _f = _a = _x = False
def __init__(self, key, error=None):
self._key = key
self._error = error
def __repr__(self):
return 'Resource: %s' % self._key
def sortKey(self):
return self._key
def tpc_begin(self, txn):
if self._error == 'tpc_begin':
raise ValueError()
self._b = True
def commit(self, txn):
if self._error == 'commit':
raise ValueError()
self._c = True
def tpc_vote(self, txn):
if self._error == 'tpc_vote':
raise ValueError()
self._v = True
def tpc_finish(self, txn):
if self._error == 'tpc_finish':
raise ValueError()
self._f = True
def abort(self, txn):
if self._error == 'abort':
raise ValueError()
self._a = True
def tpc_abort(self, txn):
if self._error == 'tpc_abort':
raise ValueError()
self._x = True
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TransactionTests),
unittest.makeSuite(MultiObjectResourceAdapterTests),
unittest.makeSuite(Test_rm_key),
unittest.makeSuite(Test_object_hint),
unittest.makeSuite(Test_oid_repr),
unittest.makeSuite(DataManagerAdapterTests),
unittest.makeSuite(SavepointTests),
unittest.makeSuite(AbortSavepointTests),
unittest.makeSuite(NoRollbackSavepointTests),
unittest.makeSuite(MiscellaneousTests),
))
import unittest
class TestAttempt(unittest.TestCase):
def _makeOne(self, manager):
from transaction._manager import Attempt
return Attempt(manager)
def test___enter__(self):
manager = DummyManager()
inst = self._makeOne(manager)
inst.__enter__()
self.assertTrue(manager.entered)
def test___exit__no_exc_no_commit_exception(self):
manager = DummyManager()
inst = self._makeOne(manager)
result = inst.__exit__(None, None, None)
self.assertFalse(result)
self.assertTrue(manager.committed)
def test___exit__no_exc_nonretryable_commit_exception(self):
manager = DummyManager(raise_on_commit=ValueError)
inst = self._makeOne(manager)
self.assertRaises(ValueError, inst.__exit__, None, None, None)
self.assertTrue(manager.committed)
self.assertTrue(manager.aborted)
def test___exit__no_exc_abort_exception_after_nonretryable_commit_exc(self):
manager = DummyManager(raise_on_abort=ValueError,
raise_on_commit=KeyError)
inst = self._makeOne(manager)
self.assertRaises(ValueError, inst.__exit__, None, None, None)
self.assertTrue(manager.committed)
self.assertTrue(manager.aborted)
def test___exit__no_exc_retryable_commit_exception(self):
from transaction.interfaces import TransientError
manager = DummyManager(raise_on_commit=TransientError)
inst = self._makeOne(manager)
result = inst.__exit__(None, None, None)
self.assertTrue(result)
self.assertTrue(manager.committed)
self.assertTrue(manager.aborted)
def test___exit__with_exception_value_retryable(self):
from transaction.interfaces import TransientError
manager = DummyManager()
inst = self._makeOne(manager)
result = inst.__exit__(TransientError, TransientError(), None)
self.assertTrue(result)
self.assertFalse(manager.committed)
self.assertTrue(manager.aborted)
def test___exit__with_exception_value_nonretryable(self):
manager = DummyManager()
inst = self._makeOne(manager)
self.assertRaises(KeyError, inst.__exit__, KeyError, KeyError(), None)
self.assertFalse(manager.committed)
self.assertTrue(manager.aborted)
class DummyManager(object):
entered = False
committed = False
aborted = False
def __init__(self, raise_on_commit=None, raise_on_abort=None):
self.raise_on_commit = raise_on_commit
self.raise_on_abort = raise_on_abort
def _retryable(self, t, v):
from transaction._manager import TransientError
return issubclass(t, TransientError)
def __enter__(self):
self.entered = True
def abort(self):
self.aborted = True
if self.raise_on_abort:
raise self.raise_on_abort
def commit(self):
self.committed = True
if self.raise_on_commit:
raise self.raise_on_commit
......@@ -23,76 +23,63 @@ API works.
These tests use a TestConnection object that implements the old API.
They check that the right methods are called and in roughly the right
order.
Common cases
------------
First, check that a basic transaction commit works.
>>> cn = TestConnection()
>>> cn.register(Object())
>>> cn.register(Object())
>>> cn.register(Object())
>>> transaction.commit()
>>> len(cn.committed)
3
>>> len(cn.aborted)
0
>>> cn.calls
['begin', 'vote', 'finish']
Second, check that a basic transaction abort works. If the
application calls abort(), then the transaction never gets into the
two-phase commit. It just aborts each object.
>>> cn = TestConnection()
>>> cn.register(Object())
>>> cn.register(Object())
>>> cn.register(Object())
>>> transaction.abort()
>>> len(cn.committed)
0
>>> len(cn.aborted)
3
>>> cn.calls
[]
Error handling
--------------
The tricky part of the implementation is recovering from an error that
occurs during the two-phase commit. We override the commit() and
abort() methods of Object to cause errors during commit.
Note that the implementation uses lists internally, so that objects
are committed in the order they are registered. (In the presence of
multiple resource managers, objects from a single resource manager are
committed in order. I'm not sure if this is an accident of the
implementation or a feature that should be supported by any
implementation.)
The order of resource managers depends on sortKey().
>>> cn = TestConnection()
>>> cn.register(Object())
>>> cn.register(CommitError())
>>> cn.register(Object())
>>> transaction.commit()
Traceback (most recent call last):
...
RuntimeError: commit
>>> len(cn.committed)
1
>>> len(cn.aborted)
3
Clean up:
>>> transaction.abort()
"""
import unittest
class BBBTests(unittest.TestCase):
def setUp(self):
from transaction import abort
abort()
tearDown = setUp
def test_basic_commit(self):
import transaction
cn = TestConnection()
cn.register(Object())
cn.register(Object())
cn.register(Object())
transaction.commit()
self.assertEqual(len(cn.committed), 3)
self.assertEqual(len(cn.aborted), 0)
self.assertEqual(cn.calls, ['begin', 'vote', 'finish'])
def test_basic_abort(self):
# If the application calls abort(), then the transaction never gets
# into the two-phase commit. It just aborts each object.
import transaction
cn = TestConnection()
cn.register(Object())
cn.register(Object())
cn.register(Object())
transaction.abort()
self.assertEqual(len(cn.committed), 0)
self.assertEqual(len(cn.aborted), 3)
self.assertEqual(cn.calls, [])
def test_tpc_error(self):
# The tricky part of the implementation is recovering from an error
# that occurs during the two-phase commit. We override the commit()
# and abort() methods of Object to cause errors during commit.
# Note that the implementation uses lists internally, so that objects
# are committed in the order they are registered. (In the presence
# of multiple resource managers, objects from a single resource
# manager are committed in order. I'm not sure if this is an
# accident of the implementation or a feature that should be
# supported by any implementation.)
# The order of resource managers depends on sortKey().
import transaction
cn = TestConnection()
cn.register(Object())
cn.register(CommitError())
cn.register(Object())
self.assertRaises(RuntimeError, transaction.commit)
self.assertEqual(len(cn.committed), 1)
self.assertEqual(len(cn.aborted), 3)
import doctest
import transaction
class Object(object):
......@@ -102,20 +89,24 @@ class Object(object):
def abort(self):
pass
class CommitError(Object):
def commit(self):
raise RuntimeError("commit")
class AbortError(Object):
def abort(self):
raise RuntimeError("abort")
class BothError(CommitError, AbortError):
pass
class TestConnection:
class TestConnection(object):
def __init__(self):
self.committed = []
......@@ -123,6 +114,7 @@ class TestConnection:
self.calls = []
def register(self, obj):
import transaction
obj._p_jar = self
transaction.get().register(obj)
......@@ -150,7 +142,6 @@ class TestConnection:
self.aborted.append(obj)
def test_suite():
return doctest.DocTestSuite()
# additional_tests is for setuptools "setup.py test" support
additional_tests = test_suite
return unittest.TestSuite((
unittest.makeSuite(BBBTests),
))
......@@ -11,82 +11,56 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Tests of savepoint feature
"""
import unittest
import doctest
def testRollbackRollsbackDataManagersThatJoinedLater():
"""
A savepoint needs to not just rollback it's savepoints, but needs to
rollback savepoints for data managers that joined savepoints after the
savepoint:
>>> import transaction
>>> from transaction.tests import savepointsample
>>> dm = savepointsample.SampleSavepointDataManager()
>>> dm['name'] = 'bob'
>>> sp1 = transaction.savepoint()
>>> dm['job'] = 'geek'
>>> sp2 = transaction.savepoint()
>>> dm['salary'] = 'fun'
>>> dm2 = savepointsample.SampleSavepointDataManager()
>>> dm2['name'] = 'sally'
>>> 'name' in dm
True
>>> 'job' in dm
True
>>> 'salary' in dm
True
>>> 'name' in dm2
True
>>> sp1.rollback()
>>> 'name' in dm
True
>>> 'job' in dm
False
>>> 'salary' in dm
False
>>> 'name' in dm2
False
"""
def test_commit_after_rollback_for_dm_that_joins_after_savepoint():
"""
There was a problem handling data managers that joined after a
savepoint. If the savepoint was rolled back and then changes made,
the dm would end up being joined twice, leading to extra tpc calls and pain.
>>> import transaction
>>> sp = transaction.savepoint()
>>> from transaction.tests import savepointsample
>>> dm = savepointsample.SampleSavepointDataManager()
>>> dm['name'] = 'bob'
>>> sp.rollback()
>>> dm['name'] = 'Bob'
>>> transaction.commit()
>>> dm['name']
'Bob'
"""
class SavepointTests(unittest.TestCase):
def testRollbackRollsbackDataManagersThatJoinedLater(self):
# A savepoint needs to not just rollback it's savepoints, but needs
# to # rollback savepoints for data managers that joined savepoints
# after the savepoint:
import transaction
from transaction.tests import savepointsample
dm = savepointsample.SampleSavepointDataManager()
dm['name'] = 'bob'
sp1 = transaction.savepoint()
dm['job'] = 'geek'
sp2 = transaction.savepoint()
dm['salary'] = 'fun'
dm2 = savepointsample.SampleSavepointDataManager()
dm2['name'] = 'sally'
self.assertTrue('name' in dm)
self.assertTrue('job' in dm)
self.assertTrue('salary' in dm)
self.assertTrue('name' in dm2)
sp1.rollback()
self.assertTrue('name' in dm)
self.assertFalse('job' in dm)
self.assertFalse('salary' in dm)
self.assertFalse('name' in dm2)
def test_commit_after_rollback_for_dm_that_joins_after_savepoint(self):
# There was a problem handling data managers that joined after a
# savepoint. If the savepoint was rolled back and then changes
# made, the dm would end up being joined twice, leading to extra
# tpc calls and pain.
import transaction
from transaction.tests import savepointsample
sp = transaction.savepoint()
dm = savepointsample.SampleSavepointDataManager()
dm['name'] = 'bob'
sp.rollback()
dm['name'] = 'Bob'
transaction.commit()
self.assertEqual(dm['name'], 'Bob')
def test_suite():
return unittest.TestSuite((
doctest.DocFileSuite('savepoint.txt'),
doctest.DocTestSuite(),
unittest.makeSuite(SavepointTests),
))
# additional_tests is for setuptools "setup.py test" support
additional_tests = test_suite
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
##############################################################################
#
# Copyright (c) 2001, 2002, 2005 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""Test transaction behavior for variety of cases.
I wrote these unittests to investigate some odd transaction
behavior when doing unittests of integrating non sub transaction
aware objects, and to insure proper txn behavior. these
tests test the transaction system independent of the rest of the
zodb.
you can see the method calls to a jar by passing the
keyword arg tracing to the modify method of a dataobject.
the value of the arg is a prefix used for tracing print calls
to that objects jar.
the number of times a jar method was called can be inspected
by looking at an attribute of the jar that is the method
name prefixed with a c (count/check).
i've included some tracing examples for tests that i thought
were illuminating as doc strings below.
TODO
add in tests for objects which are modified multiple times,
for example an object that gets modified in multiple sub txns.
"""
from doctest import DocTestSuite, DocFileSuite, IGNORE_EXCEPTION_DETAIL
import struct
import sys
import unittest
import transaction
_ADDRESS_MASK = 256 ** struct.calcsize('P')
def positive_id(obj):
"""Return id(obj) as a non-negative integer."""
result = id(obj)
if result < 0:
result += _ADDRESS_MASK
assert result > 0
return result
class TransactionTests(unittest.TestCase):
def setUp(self):
mgr = self.transaction_manager = transaction.TransactionManager()
self.sub1 = DataObject(mgr)
self.sub2 = DataObject(mgr)
self.sub3 = DataObject(mgr)
self.nosub1 = DataObject(mgr, nost=1)
# basic tests with two sub trans jars
# really we only need one, so tests for
# sub1 should identical to tests for sub2
def testTransactionCommit(self):
self.sub1.modify()
self.sub2.modify()
self.transaction_manager.commit()
assert self.sub1._p_jar.ccommit_sub == 0
assert self.sub1._p_jar.ctpc_finish == 1
def testTransactionAbort(self):
self.sub1.modify()
self.sub2.modify()
self.transaction_manager.abort()
assert self.sub2._p_jar.cabort == 1
def testTransactionNote(self):
t = self.transaction_manager.get()
t.note('This is a note.')
self.assertEqual(t.description, 'This is a note.')
t.note('Another.')
self.assertEqual(t.description, 'This is a note.\nAnother.')
t.abort()
# repeat adding in a nonsub trans jars
def testNSJTransactionCommit(self):
self.nosub1.modify()
self.transaction_manager.commit()
assert self.nosub1._p_jar.ctpc_finish == 1
def testNSJTransactionAbort(self):
self.nosub1.modify()
self.transaction_manager.abort()
assert self.nosub1._p_jar.ctpc_finish == 0
assert self.nosub1._p_jar.cabort == 1
### Failure Mode Tests
#
# ok now we do some more interesting
# tests that check the implementations
# error handling by throwing errors from
# various jar methods
###
# first the recoverable errors
def testExceptionInAbort(self):
self.sub1._p_jar = BasicJar(errors='abort')
self.nosub1.modify()
self.sub1.modify(nojar=1)
self.sub2.modify()
try:
self.transaction_manager.abort()
except TestTxnException: pass
assert self.nosub1._p_jar.cabort == 1
assert self.sub2._p_jar.cabort == 1
def testExceptionInCommit(self):
self.sub1._p_jar = BasicJar(errors='commit')
self.nosub1.modify()
self.sub1.modify(nojar=1)
try:
self.transaction_manager.commit()
except TestTxnException: pass
assert self.nosub1._p_jar.ctpc_finish == 0
assert self.nosub1._p_jar.ccommit == 1
assert self.nosub1._p_jar.ctpc_abort == 1
def testExceptionInTpcVote(self):
self.sub1._p_jar = BasicJar(errors='tpc_vote')
self.nosub1.modify()
self.sub1.modify(nojar=1)
try:
self.transaction_manager.commit()
except TestTxnException: pass
assert self.nosub1._p_jar.ctpc_finish == 0
assert self.nosub1._p_jar.ccommit == 1
assert self.nosub1._p_jar.ctpc_abort == 1
assert self.sub1._p_jar.ctpc_abort == 1
def testExceptionInTpcBegin(self):
"""
ok this test reveals a bug in the TM.py
as the nosub tpc_abort there is ignored.
nosub calling method tpc_begin
nosub calling method commit
sub calling method tpc_begin
sub calling method abort
sub calling method tpc_abort
nosub calling method tpc_abort
"""
self.sub1._p_jar = BasicJar(errors='tpc_begin')
self.nosub1.modify()
self.sub1.modify(nojar=1)
try:
self.transaction_manager.commit()
except TestTxnException:
pass
assert self.nosub1._p_jar.ctpc_abort == 1
assert self.sub1._p_jar.ctpc_abort == 1
def testExceptionInTpcAbort(self):
self.sub1._p_jar = BasicJar(errors=('tpc_abort', 'tpc_vote'))
self.nosub1.modify()
self.sub1.modify(nojar=1)
try:
self.transaction_manager.commit()
except TestTxnException:
pass
assert self.nosub1._p_jar.ctpc_abort == 1
# last test, check the hosing mechanism
## def testHoserStoppage(self):
## # It's hard to test the "hosed" state of the database, where
## # hosed means that a failure occurred in the second phase of
## # the two phase commit. It's hard because the database can
## # recover from such an error if it occurs during the very first
## # tpc_finish() call of the second phase.
## for obj in self.sub1, self.sub2:
## j = HoserJar(errors='tpc_finish')
## j.reset()
## obj._p_jar = j
## obj.modify(nojar=1)
## try:
## transaction.commit()
## except TestTxnException:
## pass
## self.assert_(Transaction.hosed)
## self.sub2.modify()
## try:
## transaction.commit()
## except Transaction.POSException.TransactionError:
## pass
## else:
## self.fail("Hosed Application didn't stop commits")
class Test_oid_repr(unittest.TestCase):
def _callFUT(self, oid):
from transaction._transaction import oid_repr
return oid_repr(oid)
def test_as_nonstring(self):
self.assertEqual(self._callFUT(123), '123')
def test_as_string_not_8_chars(self):
self.assertEqual(self._callFUT('a'), "'a'")
def test_as_string_z64(self):
s = '\0'*8
self.assertEqual(self._callFUT(s), '0x00')
def test_as_string_all_Fs(self):
s = '\1'*8
self.assertEqual(self._callFUT(s), '0x0101010101010101')
class DataObject:
def __init__(self, transaction_manager, nost=0):
self.transaction_manager = transaction_manager
self.nost = nost
self._p_jar = None
def modify(self, nojar=0, tracing=0):
if not nojar:
if self.nost:
self._p_jar = BasicJar(tracing=tracing)
else:
self._p_jar = BasicJar(tracing=tracing)
self.transaction_manager.get().join(self._p_jar)
class TestTxnException(Exception):
pass
class BasicJar:
def __init__(self, errors=(), tracing=0):
if not isinstance(errors, tuple):
errors = errors,
self.errors = errors
self.tracing = tracing
self.cabort = 0
self.ccommit = 0
self.ctpc_begin = 0
self.ctpc_abort = 0
self.ctpc_vote = 0
self.ctpc_finish = 0
self.cabort_sub = 0
self.ccommit_sub = 0
def __repr__(self):
return "<%s %X %s>" % (self.__class__.__name__,
positive_id(self),
self.errors)
def sortKey(self):
# All these jars use the same sort key, and Python's list.sort()
# is stable. These two
return self.__class__.__name__
def check(self, method):
if self.tracing:
print('%s calling method %s'%(str(self.tracing),method))
if method in self.errors:
raise TestTxnException("error %s" % method)
## basic jar txn interface
def abort(self, *args):
self.check('abort')
self.cabort += 1
def commit(self, *args):
self.check('commit')
self.ccommit += 1
def tpc_begin(self, txn, sub=0):
self.check('tpc_begin')
self.ctpc_begin += 1
def tpc_vote(self, *args):
self.check('tpc_vote')
self.ctpc_vote += 1
def tpc_abort(self, *args):
self.check('tpc_abort')
self.ctpc_abort += 1
def tpc_finish(self, *args):
self.check('tpc_finish')
self.ctpc_finish += 1
class HoserJar(BasicJar):
# The HoserJars coordinate their actions via the class variable
# committed. The check() method will only raise its exception
# if committed > 0.
committed = 0
def reset(self):
# Calling reset() on any instance will reset the class variable.
HoserJar.committed = 0
def check(self, method):
if HoserJar.committed > 0:
BasicJar.check(self, method)
def tpc_finish(self, *args):
self.check('tpc_finish')
self.ctpc_finish += 1
HoserJar.committed += 1
def test_join():
"""White-box test of the join method
The join method is provided for "backward-compatability" with ZODB 4
data managers.
The argument to join must be a zodb4 data manager,
transaction.interfaces.IDataManager.
>>> from transaction.tests.sampledm import DataManager
>>> from transaction._transaction import DataManagerAdapter
>>> t = transaction.Transaction()
>>> dm = DataManager()
>>> t.join(dm)
The end result is that a data manager adapter is one of the
transaction's objects:
>>> isinstance(t._resources[0], DataManagerAdapter)
True
>>> t._resources[0]._datamanager is dm
True
"""
def hook():
pass
def test_addBeforeCommitHook():
"""Test addBeforeCommitHook.
Let's define a hook to call, and a way to see that it was called.
>>> log = []
>>> def reset_log():
... del log[:]
>>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
... log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))
Now register the hook with a transaction.
>>> from transaction.compat import func_name
>>> import transaction
>>> t = transaction.begin()
>>> t.addBeforeCommitHook(hook, '1')
We can see that the hook is indeed registered.
>>> [(func_name(hook), args, kws)
... for hook, args, kws in t.getBeforeCommitHooks()]
[('hook', ('1',), {})]
When transaction commit starts, the hook is called, with its
arguments.
>>> log
[]
>>> t.commit()
>>> log
["arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
A hook's registration is consumed whenever the hook is called. Since
the hook above was called, it's no longer registered:
>>> len(list(t.getBeforeCommitHooks()))
0
>>> transaction.commit()
>>> log
[]
The hook is only called for a full commit, not for a savepoint.
>>> t = transaction.begin()
>>> t.addBeforeCommitHook(hook, 'A', dict(kw1='B'))
>>> dummy = t.savepoint()
>>> log
[]
>>> t.commit()
>>> log
["arg 'A' kw1 'B' kw2 'no_kw2'"]
>>> reset_log()
If a transaction is aborted, no hook is called.
>>> t = transaction.begin()
>>> t.addBeforeCommitHook(hook, ["OOPS!"])
>>> transaction.abort()
>>> log
[]
>>> transaction.commit()
>>> log
[]
The hook is called before the commit does anything, so even if the
commit fails the hook will have been called. To provoke failures in
commit, we'll add failing resource manager to the transaction.
>>> class CommitFailure(Exception):
... pass
>>> class FailingDataManager:
... def tpc_begin(self, txn, sub=False):
... raise CommitFailure('failed')
... def abort(self, txn):
... pass
>>> t = transaction.begin()
>>> t.join(FailingDataManager())
>>> t.addBeforeCommitHook(hook, '2')
>>> t.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
CommitFailure: failed
>>> log
["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
Let's register several hooks.
>>> t = transaction.begin()
>>> t.addBeforeCommitHook(hook, '4', dict(kw1='4.1'))
>>> t.addBeforeCommitHook(hook, '5', dict(kw2='5.2'))
They are returned in the same order by getBeforeCommitHooks.
>>> [(func_name(hook), args, kws) #doctest: +NORMALIZE_WHITESPACE
... for hook, args, kws in t.getBeforeCommitHooks()]
[('hook', ('4',), {'kw1': '4.1'}),
('hook', ('5',), {'kw2': '5.2'})]
And commit also calls them in this order.
>>> t.commit()
>>> len(log)
2
>>> log #doctest: +NORMALIZE_WHITESPACE
["arg '4' kw1 '4.1' kw2 'no_kw2'",
"arg '5' kw1 'no_kw1' kw2 '5.2'"]
>>> reset_log()
While executing, a hook can itself add more hooks, and they will all
be called before the real commit starts.
>>> def recurse(txn, arg):
... log.append('rec' + str(arg))
... if arg:
... txn.addBeforeCommitHook(hook, '-')
... txn.addBeforeCommitHook(recurse, (txn, arg-1))
>>> t = transaction.begin()
>>> t.addBeforeCommitHook(recurse, (t, 3))
>>> transaction.commit()
>>> log #doctest: +NORMALIZE_WHITESPACE
['rec3',
"arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec2',
"arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec1',
"arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec0']
>>> reset_log()
"""
def test_addAfterCommitHook():
"""Test addAfterCommitHook.
Let's define a hook to call, and a way to see that it was called.
>>> log = []
>>> def reset_log():
... del log[:]
>>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
... log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2))
Now register the hook with a transaction.
>>> from transaction.compat import func_name
>>> import transaction
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, '1')
We can see that the hook is indeed registered.
>>> [(func_name(hook), args, kws)
... for hook, args, kws in t.getAfterCommitHooks()]
[('hook', ('1',), {})]
When transaction commit is done, the hook is called, with its
arguments.
>>> log
[]
>>> t.commit()
>>> log
["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
A hook's registration is consumed whenever the hook is called. Since
the hook above was called, it's no longer registered:
>>> len(list(t.getAfterCommitHooks()))
0
>>> transaction.commit()
>>> log
[]
The hook is only called after a full commit, not for a savepoint.
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, 'A', dict(kw1='B'))
>>> dummy = t.savepoint()
>>> log
[]
>>> t.commit()
>>> log
["True arg 'A' kw1 'B' kw2 'no_kw2'"]
>>> reset_log()
If a transaction is aborted, no hook is called.
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, ["OOPS!"])
>>> transaction.abort()
>>> log
[]
>>> transaction.commit()
>>> log
[]
The hook is called after the commit is done, so even if the
commit fails the hook will have been called. To provoke failures in
commit, we'll add failing resource manager to the transaction.
>>> class CommitFailure(Exception):
... pass
>>> class FailingDataManager:
... def tpc_begin(self, txn):
... raise CommitFailure('failed')
... def abort(self, txn):
... pass
>>> t = transaction.begin()
>>> t.join(FailingDataManager())
>>> t.addAfterCommitHook(hook, '2')
>>> t.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
CommitFailure: failed
>>> log
["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
>>> reset_log()
Let's register several hooks.
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, '4', dict(kw1='4.1'))
>>> t.addAfterCommitHook(hook, '5', dict(kw2='5.2'))
They are returned in the same order by getAfterCommitHooks.
>>> [(func_name(hook), args, kws) #doctest: +NORMALIZE_WHITESPACE
... for hook, args, kws in t.getAfterCommitHooks()]
[('hook', ('4',), {'kw1': '4.1'}),
('hook', ('5',), {'kw2': '5.2'})]
And commit also calls them in this order.
>>> t.commit()
>>> len(log)
2
>>> log #doctest: +NORMALIZE_WHITESPACE
["True arg '4' kw1 '4.1' kw2 'no_kw2'",
"True arg '5' kw1 'no_kw1' kw2 '5.2'"]
>>> reset_log()
While executing, a hook can itself add more hooks, and they will all
be called before the real commit starts.
>>> def recurse(status, txn, arg):
... log.append('rec' + str(arg))
... if arg:
... txn.addAfterCommitHook(hook, '-')
... txn.addAfterCommitHook(recurse, (txn, arg-1))
>>> t = transaction.begin()
>>> t.addAfterCommitHook(recurse, (t, 3))
>>> transaction.commit()
>>> log #doctest: +NORMALIZE_WHITESPACE
['rec3',
"True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec2',
"True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec1',
"True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
'rec0']
>>> reset_log()
If an after commit hook is raising an exception then it will log a
message at error level so that if other hooks are registered they
can be executed. We don't support execution dependencies at this level.
>>> mgr = transaction.TransactionManager()
>>> do = DataObject(mgr)
>>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
... raise TypeError("Fake raise")
>>> t = transaction.begin()
>>> t.addAfterCommitHook(hook, ('-', 1))
>>> t.addAfterCommitHook(hookRaise, ('-', 2))
>>> t.addAfterCommitHook(hook, ('-', 3))
>>> transaction.commit()
>>> log
["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"]
>>> reset_log()
Test that the associated transaction manager has been cleanup when
after commit hooks are registered
>>> mgr = transaction.TransactionManager()
>>> do = DataObject(mgr)
>>> t = transaction.begin()
>>> t._manager._txn is not None
True
>>> t.addAfterCommitHook(hook, ('-', 1))
>>> transaction.commit()
>>> log
["True arg '-' kw1 1 kw2 'no_kw2'"]
>>> t._manager._txn is not None
False
>>> reset_log()
"""
def bug239086():
"""
The original implementation of thread transaction manager made
invalid assumptions about thread ids.
>>> import transaction.tests.savepointsample
>>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
>>> list(dm.keys())
[]
>>> class Sync:
... def __init__(self, label):
... self.label = label
... def beforeCompletion(self, t):
... print('%s %s' % (self.label, 'before'))
... def afterCompletion(self, t):
... print('%s %s' % (self.label, 'after'))
... def newTransaction(self, t):
... print('%s %s' % (self.label, 'new'))
>>> sync = Sync(1)
>>> import threading
>>> def run_in_thread(f):
... t = threading.Thread(target=f)
... t.start()
... t.join()
>>> @run_in_thread
... def first():
... transaction.manager.registerSynch(sync)
... transaction.manager.begin()
... dm['a'] = 1
1 new
>>> @run_in_thread
... def second():
... transaction.abort() # should do nothing.
>>> list(dm.keys())
['a']
>>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
>>> list(dm.keys())
[]
>>> @run_in_thread
... def first():
... dm['a'] = 1
>>> transaction.abort() # should do nothing
>>> list(dm.keys())
['a']
"""
def test_suite():
suite = unittest.TestSuite((
DocFileSuite('doom.txt'),
DocTestSuite(),
unittest.makeSuite(TransactionTests),
unittest.makeSuite(Test_oid_repr),
))
if sys.version_info >= (2, 6):
suite.addTest(DocFileSuite('convenience.txt',
optionflags=IGNORE_EXCEPTION_DETAIL))
return suite
# additional_tests is for setuptools "setup.py test" support
additional_tests = test_suite
if __name__ == '__main__':
unittest.TextTestRunner().run(test_suite())
......@@ -11,15 +11,12 @@
# FOR A PARTICULAR PURPOSE
#
##############################################################################
import unittest
from transaction.weakset import WeakSet
class Dummy:
pass
class WeakSetTests(unittest.TestCase):
def test_contains(self):
from transaction.weakset import WeakSet
w = WeakSet()
dummy = Dummy()
w.add(dummy)
......@@ -29,6 +26,7 @@ class WeakSetTests(unittest.TestCase):
def test_len(self):
import gc
from transaction.weakset import WeakSet
w = WeakSet()
d1 = Dummy()
d2 = Dummy()
......@@ -40,6 +38,7 @@ class WeakSetTests(unittest.TestCase):
self.assertEqual(len(w), 1)
def test_remove(self):
from transaction.weakset import WeakSet
w = WeakSet()
dummy = Dummy()
w.add(dummy)
......@@ -49,6 +48,7 @@ class WeakSetTests(unittest.TestCase):
def test_as_weakref_list(self):
import gc
from transaction.weakset import WeakSet
w = WeakSet()
dummy = Dummy()
dummy2 = Dummy()
......@@ -64,6 +64,7 @@ class WeakSetTests(unittest.TestCase):
self.assertEqual(set(L), set([dummy, dummy2]))
def test_map(self):
from transaction.weakset import WeakSet
w = WeakSet()
dummy = Dummy()
dummy2 = Dummy()
......@@ -77,10 +78,10 @@ class WeakSetTests(unittest.TestCase):
for thing in dummy, dummy2, dummy3:
self.assertEqual(thing.poked, 1)
class Dummy:
pass
def test_suite():
return unittest.makeSuite(WeakSetTests)
if __name__ == '__main__':
unittest.main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment