Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Z
Zope
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Kirill Smelkov
Zope
Commits
db30982b
Commit
db30982b
authored
May 11, 2004
by
root
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use tagged version of src/transaction
parent
643a9757
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
2462 additions
and
0 deletions
+2462
-0
lib/python/transaction/DEPENDENCIES.cfg
lib/python/transaction/DEPENDENCIES.cfg
+2
-0
lib/python/transaction/README.txt
lib/python/transaction/README.txt
+14
-0
lib/python/transaction/__init__.py
lib/python/transaction/__init__.py
+33
-0
lib/python/transaction/_manager.py
lib/python/transaction/_manager.py
+93
-0
lib/python/transaction/_transaction.py
lib/python/transaction/_transaction.py
+582
-0
lib/python/transaction/interfaces.py
lib/python/transaction/interfaces.py
+120
-0
lib/python/transaction/notes.txt
lib/python/transaction/notes.txt
+269
-0
lib/python/transaction/tests/__init__.py
lib/python/transaction/tests/__init__.py
+1
-0
lib/python/transaction/tests/abstestIDataManager.py
lib/python/transaction/tests/abstestIDataManager.py
+63
-0
lib/python/transaction/tests/test_SampleDataManager.py
lib/python/transaction/tests/test_SampleDataManager.py
+412
-0
lib/python/transaction/tests/test_register_compat.py
lib/python/transaction/tests/test_register_compat.py
+150
-0
lib/python/transaction/tests/test_transaction.py
lib/python/transaction/tests/test_transaction.py
+647
-0
lib/python/transaction/tests/test_util.py
lib/python/transaction/tests/test_util.py
+25
-0
lib/python/transaction/util.py
lib/python/transaction/util.py
+51
-0
No files found.
lib/python/transaction/DEPENDENCIES.cfg
0 → 100644
View file @
db30982b
ZODB
zope.interface
lib/python/transaction/README.txt
0 → 100644
View file @
db30982b
This package is currently a facade of the ZODB.Transaction module.
It exists to support:
- Application code that uses the ZODB 4 transaction API
- ZODB4-style data managers (transaction.interfaces.IDataManager)
Note that the data manager API, transaction.interfaces.IDataManager,
is syntactically simple, but semantically complex. The semantics
were not easy to express in the interface. This could probably use
more work. The semantics are presented in detail through examples of
a sample data manager in transaction.tests.test_SampleDataManager.
lib/python/transaction/__init__.py
0 → 100644
View file @
db30982b
############################################################################
#
# Copyright (c) 2001, 2002, 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
############################################################################
from
transaction._transaction
import
Transaction
from
transaction._manager
import
TransactionManager
,
ThreadTransactionManager
manager
=
ThreadTransactionManager
()
def
get
():
return
manager
.
get
()
def
begin
():
return
manager
.
begin
()
def
commit
(
sub
=
False
):
manager
.
get
().
commit
(
sub
)
def
abort
(
sub
=
False
):
manager
.
get
().
abort
(
sub
)
# XXX Issue deprecation warning if this variant is used?
get_transaction
=
get
lib/python/transaction/_manager.py
0 → 100644
View file @
db30982b
############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
############################################################################
"""A TransactionManager controls transaction boundaries.
It coordinates application code and resource managers, so that they
are associated with the right transaction.
"""
import
thread
from
transaction._transaction
import
Transaction
class
TransactionManager
(
object
):
def
__init__
(
self
):
self
.
_txn
=
None
self
.
_synchs
=
[]
def
begin
(
self
):
if
self
.
_txn
is
not
None
:
self
.
_txn
.
abort
()
self
.
_txn
=
Transaction
(
self
.
_synchs
,
self
)
return
self
.
_txn
def
get
(
self
):
if
self
.
_txn
is
None
:
self
.
_txn
=
Transaction
(
self
.
_synchs
,
self
)
return
self
.
_txn
def
free
(
self
,
txn
):
assert
txn
is
self
.
_txn
self
.
_txn
=
None
def
registerSynch
(
self
,
synch
):
self
.
_synchs
.
append
(
synch
)
def
unregisterSynch
(
self
,
synch
):
self
.
_synchs
.
remove
(
synch
)
class
ThreadTransactionManager
(
object
):
"""Thread-aware transaction manager.
Each thread is associated with a unique transaction.
"""
def
__init__
(
self
):
# _threads maps thread ids to transactions
self
.
_txns
=
{}
# _synchs maps a thread id to a list of registered synchronizers.
# The list is passed to the Transaction constructor, because
# it needs to call the synchronizers when it commits.
self
.
_synchs
=
{}
def
begin
(
self
):
tid
=
thread
.
get_ident
()
txn
=
self
.
_txns
.
get
(
tid
)
if
txn
is
not
None
:
txn
.
abort
()
txn
=
self
.
_txns
[
tid
]
=
Transaction
(
self
.
_synchs
.
get
(
tid
),
self
)
return
txn
def
get
(
self
):
tid
=
thread
.
get_ident
()
txn
=
self
.
_txns
.
get
(
tid
)
if
txn
is
None
:
txn
=
self
.
_txns
[
tid
]
=
Transaction
(
self
.
_synchs
.
get
(
tid
),
self
)
return
txn
def
free
(
self
,
txn
):
tid
=
thread
.
get_ident
()
assert
txn
is
self
.
_txns
.
get
(
tid
)
del
self
.
_txns
[
tid
]
def
registerSynch
(
self
,
synch
):
tid
=
thread
.
get_ident
()
L
=
self
.
_synchs
.
setdefault
(
tid
,
[])
L
.
append
(
synch
)
def
unregisterSynch
(
self
,
synch
):
tid
=
thread
.
get_ident
()
L
=
self
.
_synchs
.
get
(
tid
)
L
.
remove
(
synch
)
lib/python/transaction/_transaction.py
0 → 100644
View file @
db30982b
############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
############################################################################
"""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. XXX explain adapter
Subtransactions
---------------
A subtransaction applies the transaction notion recursively. It
allows a set of modifications within a transaction to be committed or
aborted as a group. A subtransaction is a strictly local activity;
its changes are not visible to any other database connection until the
top-level transaction commits. In addition to its use to organize a
large transaction, subtransactions can be used to optimize memory use.
ZODB must keep modified objects in memory until a transaction commits
and it can write the changes to the storage. A subtransaction uses a
temporary disk storage for its commits, allowing modified objects to
be flushed from memory when the subtransaction commits.
The commit() and abort() methods take an optional subtransaction
argument that defaults to false. If it is a true, the operation is
performed on a subtransaction.
Subtransactions add a lot of complexity to the transaction
implementation. Some resource managers support subtransactions, but
they are not required to. (ZODB Connection is the only standard
resource manager that supports subtransactions.) Resource managers
that do support subtransactions implement abort_sub() and commit_sub()
methods and support a second argument to tpc_begin().
The second argument to tpc_begin() indicates that a subtransaction
commit is beginning (if it is true). In a subtransaction, there is no
tpc_vote() call. (XXX I don't have any idea why.) The tpc_finish()
or tpc_abort() call applies just to that subtransaction.
Once a resource manager is involved in a subtransaction, all
subsequent transactions will be treated as subtransactions until
abort_sub() or commit_sub() is called. abort_sub() will undo all the
changes of the subtransactions. commit_sub() will begin a top-level
transaction and store all the changes from subtransactions. After
commit_sub(), the transaction must still call tpc_vote() and
tpc_finish().
If the resource manager does not support subtransactions, nothing
happens when the subtransaction commits. Instead, the resource
manager is put on a list of managers to commit when the actual
top-level transaction commits. If this happens, it will not be
possible to abort subtransactions.
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, subtransaction=False)
2. commit(txn)
3. tpc_vote(txn)
4. tpc_finish(txn)
Subtransaction commit
---------------------
When a subtransaction commits, the protocol is different.
1. tpc_begin() is passed a second argument, which indicates that a
subtransaction is being committed.
2. tpc_vote() is not called.
Once a subtransaction has been committed, the top-level transaction
commit will start with a commit_sub() call instead of a tpc_begin()
call.
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. abort_sub() is called if the
resource manager was involved in a subtransaction.
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.
XXX This code isn't tested.
"""
import
logging
import
sys
import
thread
_marker
=
object
()
# The point of this is to avoid hiding exceptions (which the builtin
# hasattr() does).
def
myhasattr
(
obj
,
attr
):
return
getattr
(
obj
,
attr
,
_marker
)
is
not
_marker
class
Status
:
ACTIVE
=
"Active"
COMMITTING
=
"Committing"
COMMITTED
=
"Committed"
ABORTING
=
"Aborting"
ABORTED
=
"Aborted"
FAILED
=
"Failed"
class
Transaction
(
object
):
def
__init__
(
self
,
synchronizers
=
None
,
manager
=
None
):
self
.
status
=
Status
.
ACTIVE
# List of resource managers, e.g. MultiObjectResourceAdapters.
self
.
_resources
=
[]
self
.
_synchronizers
=
synchronizers
or
[]
self
.
_manager
=
manager
# _adapters: Connection/_p_jar -> MultiObjectResourceAdapter[Sub]
self
.
_adapters
=
{}
self
.
_voted
=
{}
# id(Connection) -> boolean, True if voted
# _voted and other dictionaries use the id() of the resource
# manager as a key, because we can't guess whether the actual
# resource managers will be safe to use as dict keys.
# The user, description, and _extension attributes are accessed
# directly by storages, leading underscore notwithstanding.
self
.
user
=
""
self
.
description
=
""
self
.
_extension
=
{}
self
.
log
=
logging
.
getLogger
(
"txn.%d"
%
thread
.
get_ident
())
self
.
log
.
debug
(
"new transaction"
)
# _sub contains all of the resource managers involved in
# subtransactions. It maps id(a resource manager) to the resource
# manager.
self
.
_sub
=
{}
# _nonsub contains all the resource managers that do not support
# subtransactions that were involved in subtransaction commits.
self
.
_nonsub
=
{}
def
join
(
self
,
resource
):
if
self
.
status
!=
Status
.
ACTIVE
:
# XXX Should it be possible to join a committing transaction?
# I think some users want it.
raise
ValueError
(
"expected txn status %r, but it's %r"
%
(
Status
.
ACTIVE
,
self
.
status
))
# XXX the prepare check is a bit of a hack, perhaps it would
# be better to use interfaces. If this is a ZODB4-style
# resource manager, it needs to be adapted, too.
if
myhasattr
(
resource
,
"prepare"
):
resource
=
DataManagerAdapter
(
resource
)
self
.
_resources
.
append
(
resource
)
def
register
(
self
,
obj
):
# The old way of registering transaction participants.
#
# register() is passed either a persisent object or a
# resource manager like the ones defined in ZODB.DB.
# If it is passed a persistent object, that object should
# be stored when the transaction commits. For other
# objects, the object implements the standard two-phase
# commit protocol.
manager
=
getattr
(
obj
,
"_p_jar"
,
obj
)
adapter
=
self
.
_adapters
.
get
(
manager
)
if
adapter
is
None
:
if
myhasattr
(
manager
,
"commit_sub"
):
adapter
=
MultiObjectResourceAdapterSub
(
manager
)
else
:
adapter
=
MultiObjectResourceAdapter
(
manager
)
adapter
.
objects
.
append
(
obj
)
self
.
_adapters
[
manager
]
=
adapter
self
.
join
(
adapter
)
else
:
# XXX comment out this expensive assert later
# Use id() to guard against proxies.
assert
id
(
obj
)
not
in
map
(
id
,
adapter
.
objects
)
adapter
.
objects
.
append
(
obj
)
# In the presence of subtransactions, an existing adapter
# might be in _adapters but not in _resources.
if
adapter
not
in
self
.
_resources
:
self
.
_resources
.
append
(
adapter
)
def
begin
(
self
):
# XXX I'm not sure how this should be implemented. Not doing
# anything now, but my best guess is: If nothing has happened
# yet, it's fine. Otherwise, abort this transaction and let
# the txn manager create a new one.
pass
def
commit
(
self
,
subtransaction
=
False
):
if
not
subtransaction
and
self
.
_sub
and
self
.
_resources
:
# This commit is for a top-level transaction that has
# previously committed subtransactions. Do one last
# subtransaction commit to clear out the current objects,
# then commit all the subjars.
self
.
commit
(
True
)
if
not
subtransaction
:
for
s
in
self
.
_synchronizers
:
s
.
beforeCompletion
(
self
)
if
not
subtransaction
:
self
.
status
=
Status
.
COMMITTING
self
.
_commitResources
(
subtransaction
)
if
subtransaction
:
self
.
_resources
=
[]
else
:
self
.
status
=
Status
.
COMMITTED
if
self
.
_manager
:
self
.
_manager
.
free
(
self
)
for
s
in
self
.
_synchronizers
:
s
.
afterCompletion
(
self
)
self
.
log
.
debug
(
"commit"
)
def
_commitResources
(
self
,
subtransaction
):
# Execute the two-phase commit protocol.
L
=
self
.
_getResourceManagers
(
subtransaction
)
try
:
for
rm
in
L
:
# If you pass subtransaction=True to tpc_begin(), it
# will create a temporary storage for the duration of
# the transaction. To signal that the top-level
# transaction is committing, you must then call
# commit_sub().
if
not
subtransaction
and
id
(
rm
)
in
self
.
_sub
:
del
self
.
_sub
[
id
(
rm
)]
rm
.
commit_sub
(
self
)
else
:
rm
.
tpc_begin
(
self
,
subtransaction
)
for
rm
in
L
:
rm
.
commit
(
self
)
self
.
log
.
debug
(
"commit %r"
%
rm
)
if
not
subtransaction
:
# Not sure why, but it is intentional that you do not
# call tpc_vote() for subtransaction commits.
for
rm
in
L
:
rm
.
tpc_vote
(
self
)
self
.
_voted
[
id
(
rm
)]
=
True
try
:
for
rm
in
L
:
rm
.
tpc_finish
(
self
)
except
:
# XXX do we need to make this warning stronger?
# XXX It would be nice if the system could be configured
# to stop committing transactions at this point.
self
.
log
.
critical
(
"A storage error occured during the second "
"phase of the two-phase commit. Resources "
"may be in an inconsistent state."
)
raise
except
:
# If an error occurs committing a transaction, we try
# to revert the changes in each of the resource managers.
# For top-level transactions, it must be freed from the
# txn manager.
t
,
v
,
tb
=
sys
.
exc_info
()
try
:
self
.
_cleanup
(
L
)
finally
:
if
not
subtransaction
:
self
.
status
=
Status
.
FAILED
if
self
.
_manager
:
self
.
_manager
.
free
(
self
)
for
s
in
self
.
_synchronizers
:
s
.
afterCompletion
(
self
)
raise
t
,
v
,
tb
def
_cleanup
(
self
,
L
):
# Called when an exception occurs during tpc_vote or tpc_finish.
for
rm
in
L
:
if
id
(
rm
)
not
in
self
.
_voted
:
try
:
rm
.
abort
(
self
)
except
Exception
:
self
.
log
.
error
(
"Error in abort() on manager %s"
,
rm
,
exc_info
=
sys
.
exc_info
())
for
rm
in
L
:
if
id
(
rm
)
in
self
.
_sub
:
try
:
rm
.
abort_sub
(
self
)
except
Exception
:
self
.
log
.
error
(
"Error in abort_sub() on manager %s"
,
rm
,
exc_info
=
sys
.
exc_info
())
else
:
try
:
rm
.
tpc_abort
(
self
)
except
Exception
:
self
.
log
.
error
(
"Error in tpc_abort() on manager %s"
,
rm
,
exc_info
=
sys
.
exc_info
())
def
_getResourceManagers
(
self
,
subtransaction
):
L
=
[]
if
subtransaction
:
# If we are in a subtransaction, make sure all resource
# managers are placed in either _sub or _nonsub. When
# the top-level transaction commits, we need to merge
# these back into the resource set.
# If a data manager doesn't support sub-transactions, we
# don't do anything with it now. (That's somewhat okay,
# because subtransactions are mostly just an
# optimization.) Save it until the top-level transaction
# commits.
for
rm
in
self
.
_resources
:
if
myhasattr
(
rm
,
"commit_sub"
):
self
.
_sub
[
id
(
rm
)]
=
rm
L
.
append
(
rm
)
else
:
self
.
_nonsub
[
id
(
rm
)]
=
rm
else
:
if
self
.
_sub
or
self
.
_nonsub
:
# Merge all of _sub, _nonsub, and _resources.
d
=
dict
(
self
.
_sub
)
d
.
update
(
self
.
_nonsub
)
# XXX I think _sub and _nonsub are disjoint, and that
# XXX _resources is empty. If so, we can simplify this code.
assert
len
(
d
)
==
len
(
self
.
_sub
)
+
len
(
self
.
_nonsub
)
assert
not
self
.
_resources
for
rm
in
self
.
_resources
:
d
[
id
(
rm
)]
=
rm
L
=
d
.
values
()
else
:
L
=
list
(
self
.
_resources
)
L
.
sort
(
rm_cmp
)
return
L
def
abort
(
self
,
subtransaction
=
False
):
if
not
subtransaction
:
for
s
in
self
.
_synchronizers
:
s
.
beforeCompletion
(
self
)
if
subtransaction
and
self
.
_nonsub
:
raise
TransactionError
(
"Resource manager does not support "
"subtransaction abort"
)
tb
=
None
for
rm
in
self
.
_resources
+
self
.
_nonsub
.
values
():
try
:
rm
.
abort
(
self
)
except
:
if
tb
is
None
:
t
,
v
,
tb
=
sys
.
exc_info
()
self
.
log
.
error
(
"Failed to abort resource manager: %s"
,
rm
,
exc_info
=
sys
.
exc_info
())
if
not
subtransaction
:
for
rm
in
self
.
_sub
.
values
():
try
:
rm
.
abort_sub
(
self
)
except
:
if
tb
is
None
:
t
,
v
,
tb
=
sys
.
exc_info
()
self
.
log
.
error
(
"Failed to abort_sub resource manager: %s"
,
rm
,
exc_info
=
sys
.
exc_info
())
if
not
subtransaction
:
if
self
.
_manager
:
self
.
_manager
.
free
(
self
)
for
s
in
self
.
_synchronizers
:
s
.
afterCompletion
(
self
)
self
.
log
.
debug
(
"abort"
)
if
tb
is
not
None
:
raise
t
,
v
,
tb
def
note
(
self
,
text
):
text
=
text
.
strip
()
if
self
.
description
:
self
.
description
+=
"
\
n
\
n
"
+
text
else
:
self
.
description
=
text
def
setUser
(
self
,
user_name
,
path
=
"/"
):
self
.
user
=
"%s %s"
%
(
path
,
user_name
)
def
setExtendedInfo
(
self
,
name
,
value
):
self
.
_extension
[
name
]
=
value
# XXX We need a better name for the adapters.
class
MultiObjectResourceAdapter
(
object
):
"""Adapt the old-style register() call to the new-style join().
With join(), a resource mananger like a Connection registers with
the transaction manager. With register(), an individual object
is passed to register().
"""
def
__init__
(
self
,
jar
):
self
.
manager
=
jar
self
.
objects
=
[]
self
.
ncommitted
=
0
def
__repr__
(
self
):
return
"<%s for %s at %s>"
%
(
self
.
__class__
.
__name__
,
self
.
manager
,
id
(
self
))
def
sortKey
(
self
):
return
self
.
manager
.
sortKey
()
def
tpc_begin
(
self
,
txn
,
sub
=
False
):
self
.
manager
.
tpc_begin
(
txn
,
sub
)
def
tpc_finish
(
self
,
txn
):
self
.
manager
.
tpc_finish
(
txn
)
def
tpc_abort
(
self
,
txn
):
self
.
manager
.
tpc_abort
(
txn
)
def
commit
(
self
,
txn
):
for
o
in
self
.
objects
:
self
.
manager
.
commit
(
o
,
txn
)
self
.
ncommitted
+=
1
def
tpc_vote
(
self
,
txn
):
self
.
manager
.
tpc_vote
(
txn
)
def
abort
(
self
,
txn
):
tb
=
None
for
o
in
self
.
objects
:
try
:
self
.
manager
.
abort
(
o
,
txn
)
except
:
# Capture the first exception and re-raise it after
# aborting all the other objects.
if
tb
is
None
:
t
,
v
,
tb
=
sys
.
exc_info
()
txn
.
log
.
error
(
"Failed to abort object: %s"
,
object_hint
(
o
),
exc_info
=
sys
.
exc_info
())
if
tb
is
not
None
:
raise
t
,
v
,
tb
class
MultiObjectResourceAdapterSub
(
MultiObjectResourceAdapter
):
"""Adapt resource managers that participate in subtransactions."""
def
commit_sub
(
self
,
txn
):
self
.
manager
.
commit_sub
(
txn
)
def
abort_sub
(
self
,
txn
):
self
.
manager
.
abort_sub
(
txn
)
def
tpc_begin
(
self
,
txn
,
sub
=
False
):
self
.
manager
.
tpc_begin
(
txn
,
sub
)
self
.
sub
=
sub
def
tpc_finish
(
self
,
txn
):
self
.
manager
.
tpc_finish
(
txn
)
if
self
.
sub
:
self
.
objects
=
[]
def
rm_cmp
(
rm1
,
rm2
):
return
cmp
(
rm1
.
sortKey
(),
rm2
.
sortKey
())
def
object_hint
(
o
):
"""Return a string describing the object.
This function does not raise an exception.
"""
from
ZODB.utils
import
oid_repr
# 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
=
getattr
(
o
,
"_p_oid"
,
_marker
)
if
oid
is
not
_marker
:
oid
=
oid_repr
(
oid
)
return
"%s oid=%s"
%
(
klass
,
oid
)
class
DataManagerAdapter
(
object
):
"""Adapt zodb 4-style data managers to zodb3 style
Adapt transaction.interfaces.IDataManager to
ZODB.interfaces.IPureDatamanager
"""
# Note that it is pretty important that this does not have a _p_jar
# attribute. This object will be registered with a zodb3 TM, which
# will then try to get a _p_jar from it, using it as the default.
# (Objects without a _p_jar are their own data managers.)
def
__init__
(
self
,
datamanager
):
self
.
_datamanager
=
datamanager
self
.
_rollback
=
None
# XXX I'm not sure why commit() doesn't do anything
def
commit
(
self
,
transaction
):
pass
def
abort
(
self
,
transaction
):
# We need to discard any changes since the last save point, or all
# changes
if
self
.
_rollback
is
None
:
# No previous savepoint, so just abort
self
.
_datamanager
.
abort
(
transaction
)
else
:
self
.
_rollback
()
def
abort_sub
(
self
,
transaction
):
self
.
_datamanager
.
abort
(
transaction
)
def
commit_sub
(
self
,
transaction
):
# Nothing to do wrt data, be we begin 2pc for the top-level
# trans
self
.
_sub
=
False
def
tpc_begin
(
self
,
transaction
,
subtransaction
=
False
):
self
.
_sub
=
subtransaction
def
tpc_abort
(
self
,
transaction
):
if
self
.
_sub
:
self
.
abort
(
self
,
transaction
)
else
:
self
.
_datamanager
.
abort
(
transaction
)
def
tpc_finish
(
self
,
transaction
):
if
self
.
_sub
:
self
.
_rollback
=
self
.
_datamanager
.
savepoint
(
transaction
).
rollback
else
:
self
.
_datamanager
.
commit
(
transaction
)
def
tpc_vote
(
self
,
transaction
):
if
not
self
.
_sub
:
self
.
_datamanager
.
prepare
(
transaction
)
lib/python/transaction/interfaces.py
0 → 100644
View file @
db30982b
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
##############################################################################
"""Transaction Interfaces
$Id: interfaces.py,v 1.8 2004/04/19 21:19:10 tim_one Exp $
"""
try
:
from
zope.interface
import
Interface
except
ImportError
:
class
Interface
:
pass
class
IDataManager
(
Interface
):
"""Data management interface for storing objects transactionally
This is currently implemented by ZODB database connections.
XXX This exists to document ZODB4 behavior, to help create some
backward-compatability support for Zope 3. New classes shouldn't
implement this. They should implement ZODB.interfaces.IDataManager
for now. Our hope is that there will eventually be an interface
like this or that this interface will evolve and become the
standard interface. There are some issues to be resolved first, like:
- Probably want separate abort methods for use in and out of
two-phase commit.
- The savepoint api may need some more thought.
"""
def
prepare
(
transaction
):
"""Perform the first phase of a 2-phase commit
The data manager prepares for commit any changes to be made
persistent. A normal return from this method indicated that
the data manager is ready to commit the changes.
The data manager must raise an exception if it is not prepared
to commit the transaction after executing prepare().
The transaction must match that used for preceeding
savepoints, if any.
"""
# This is equivalent to zodb3's tpc_begin, commit, and
# tpc_vote combined.
def
abort
(
transaction
):
"""Abort changes made by transaction
This may be called before two-phase commit or in the second
phase of two-phase commit.
The transaction must match that used for preceeding
savepoints, if any.
"""
# This is equivalent to *both* zodb3's abort and tpc_abort
# calls. This should probably be split into 2 methods.
def
commit
(
transaction
):
"""Finish two-phase commit
The prepare method must be called, with the same transaction,
before calling commit.
"""
# This is equivalent to zodb3's tpc_finish
def
savepoint
(
transaction
):
"""Do tentative commit of changes to this point.
Should return an object implementing IRollback that can be used
to rollback to the savepoint.
Note that (unlike zodb3) this doesn't use a 2-phase commit
protocol. If this call fails, or if a rollback call on the
result fails, the (containing) transaction should be
aborted. Aborting the containing transaction is *not* the
responsibility of the data manager, however.
An implementation that doesn't support savepoints should
implement this method by returning a rollback implementation
that always raises an error when it's rollback method is
called. The savepoing method shouldn't raise an error. This
way, transactions that create savepoints can proceed as long
as an attempt is never made to roll back a savepoint.
"""
class
IRollback
(
Interface
):
def
rollback
():
"""Rollback changes since savepoint.
IOW, rollback to the last savepoint.
It is an error to rollback to a savepoint if:
- An earlier savepoint within the same transaction has been
rolled back to, or
- The transaction has ended.
"""
lib/python/transaction/notes.txt
0 → 100644
View file @
db30982b
[more info may (or may not) be added to
http://zope.org/Wikis/ZODB/ReviseTransactionAPI
]
Notes on a future transaction API
=================================
I did a brief review of the current transaction APIs from ZODB 3 and
ZODB 4, considering some of the issues that have come up since last
winter when most of the initial design and implementation of ZODB 4's
transaction API was done.
Participants
------------
There are four participants in the transaction APIs.
1. Application -- Some application code is ultimately in charge of the
transaction process. It uses transactional resources, decides the
scope of individual transactions, and commits or aborts transactions.
2. Resource Manager -- Typically library or framework code that provides
transactional access to some resource -- a ZODB database, a relational
database, or some other resource. It provides an API for application
code that isn't defined by the transaction framework. It collaborates
with the transaction manager to find the current transaction. It
collaborates with the transaction for registration, notification, and
for committing changes.
The ZODB Connection is a resource manager. In ZODB 4, it is called a
data manager. In ZODB 3, it is called a jar. In other literature,
resource manager seems to be common.
3. Transaction -- coordinates the actions of application and resource
managers for a particular activity. The transaction usually has a
short lifetime. The application begins it, resources register with it
as the application runs, then it finishes with a commit or abort.
4. Transaction Manager -- coordinates the use of transaction. The
transaction manager provides policies for associating resource
managers with specific transactions. The question "What is the
current transaction?" is answered by the transaction manager.
I'm taking as a starting point the transaction API that was defined
for ZODB 4. I reviewed it again after a lot of time away, and I still
think it's on the right track.
Current transaction
-------------------
The first question is "What is the current transaction?" This
question is decided by the transaction manager. An application could
chose an application manager that suites its need best.
In the current ZODB, the transaction manager is essentially the
implementation of ZODB.Transaction.get_transaction() and the
associated thread id -> txn dict. I think we can encapsulate this
policy an a first-class object and allow applications to decide which
one they want to use. By default, a thread-based txn manager would be
provided.
The other responsibility of the transaction manager is to decide when
to start a new transaction. The current ZODB transaction manager
starts one whenever a client calls get() and there is no current
transaction. I think there could be some benefit to an explicit new()
operation that will always create a new transaction. A particular
manager could implement the policy that get() called before new()
returns None or raises an exception.
Basic transaction API
---------------------
A transaction module or package can export a very simple API for
interacting with transactions. It hides most of the complexity from
applications that want to use the standard Zope policies. Here's a
sketch of an implementation:
_mgr = TransactionManager()
def get():
"""Return the current transaction."""
return _mgr.get()
def new():
"""Return a new transaction."""
return _mgr.new()
def commit():
"""Commit the current transaction."""
_mgr.get().commit()
def abort():
"""Abort the current transaction."""
_mgr.get().abort()
Application code can just import the transaction module to use the
get(), new(), abort(), and commit() methods.
The individual transaction objects should have a register() method
that is used by a resource manager to register that it has
modifications for this transaction. It's part of the basic API, but
not the basic user API.
Extended transaction API
------------------------
There are a few other methods that might make sense on a transaction:
status() -- return a code or string indicating what state the
transaction is in -- begin, aborted, committed, etc.
note() -- add metadata to txn
The transaction module should have a mechanism for installing a new
transaction manager.
Suspend and resume
------------------
If the transaction manager's job is to decide what the current
transaction is, then it would make sense to have suspend() and
resume() APIs that allow the current activity to be stopped for a
time. The goal of these APIs is to allow more control over
coordination.
It seems like user code would call suspend() and resume() on
individual transaction objects, which would interact with the
transaction manager.
If suspend() and resume() are supported, then we need to think about
whether those events need to be communicated to the resource
managers.
This is a new feature that isn't needed for ZODB 3.3.
Registration and notification
-----------------------------
The transaction object coordinates the activities of resource
managers. When a managed resource is modified, its manager must
register with the current transaction. (It's an error to modify an
object when there is no transaction?)
When the transaction commits or aborts, the transaction calls back to
each registered resource manager. The callbacks form the two-phase
commit protocol. I like the ZODB 4 names and approach prepare() (does
tpc_begin through tpc_vote on the storage).
A resource manager does not register with a transaction if none of its
resources are modified. Some resource managers would like to know
about transaction boundaries anyway. A ZODB Connection would like to
process invalidations at every commit, even if none of its objects
were modified.
It's not clear what the notification interface should look like or
what events are of interest. In theory, transaction begin, abort, and
commit are all interesting; perhaps a combined abort-or-commit event
would be useful. The ZODB use case only needs one event.
The java transaction API has beforeCompletion and afterCompletion,
where after gets passed a status code to indicate abort or commit.
I think these should be sufficient.
Nested transactions / savepoints
--------------------------------
ZODB 3 and ZODB 4 each have a limited form of nested transactions.
They are called subtransactions in ZODB 3 and savepoints in ZODB 4.
The essential mechanism is the same: At the time of subtransaction is
committed, all the modifications up to that time are written out to a
temporary file. The application can later revert to that saved state
or commit the main transaction, which copies modifications from the
temporary file to the real storage.
The savepoint mechanism can be used to implement the subtransaction
model, by creating a savepoint every time a subtransaction starts or
ends.
If a resource manager joins a transaction after a savepoint, we need
to create an initial savepoint for the new resource manager that will
rollback all its changes. If the new resource manager doesn't support
savepoints, we probably need to mark earlier savepoints as invalid.
There are some edges cases to work out here.
It's not clear how nested transactions affect the transaction manager
API. If we just use savepoint(), then there's no issue to sort out.
A nested transaction API may be more convenient. One possibility is
to pass a transaction object to new() indicating that the new
transaction is a child of the current transaction. Example:
transaction.new(transaction.get())
That seems rather wordy. Perhaps:
transaction.child()
where this creates a new nested transaction that is a child of the
current one, raising an exception if there is no current transaction.
This illustrates that a subtransaction feature could create new
requirements for the transaction manager API.
The current ZODB 3 API is that calling commit(1) or commit(True) means
"commit a subtransaction." abort() has the same API. We need to
support this API for backwards compatibility. A new API would be a
new feature that isn't necessary for ZODB 3.3.
ZODB Connection and Transactions
--------------------------------
The Connection has three interactions with a transaction manager.
First, it registers itself with the transaction manager for
synchronization messages. Second, it registers with the current
transaction the first time an object is modified in that transaction.
Third, there is an option to explicitly pass a transaction manager to
the connection constructor via DB.open(); the connection always uses
this transaction manager, regardless of the default manager.
Deadlock and recovery
---------------------
ZODB uses a global sort order to prevent deadlock when it commits
transactions involving multiple resource managers. The resource
manager must define a sortKey() method that provides a global ordering
for resource managers. The sort code doesn't exist in ZODB 4, but
could be added fairly easily.
The transaction managers don't support recovery, where recovery means
restoring a system to a consistent state after a failure during the
second phase of two-phase commit. When a failure occurs in the second
phase, some transaction participations may not know the outcome of the
transaction. (It would be cool to support recovery, but that's not
being discussed now.)
In the absence of real recovery manager means that our transaction
commit implementation needs to play many tricks to avoid the need for
recovery (pseudo-recovery). For example, if the first resource
manager fails in the second phase, we attempt to abort all the other
resource managers. (This isn't strictly correct, because we don't know the
status of the first resource manager if it fails.) If we used
something more like the ZODB 4 implementation, we'd need to make sure
all the pseudo-recovery work is done in the new implementation.
Closing resource managers
-------------------------
The ZODB Connection is explicitly opened and closed by the
application; other resource managers probably get closed to. The
relationship between transactions and closed resource managers is
undefined in the current API. A transaction will probably fail if the
Connection is closed, or succeed by accident if the Connection is
re-opened.
The resource manager - transaction API should include some means for
dealing with close. The likely approach is to raise an error if you
close a resource manager that is currently registered with a
transaction.
First steps
-----------
I would definitely like to see some things in ZODB 3.3:
- simplified module-level transaction calls
- notifications for abort-commit event
- restructured Connection to track modified objects itself
- explicit transaction manager object
lib/python/transaction/tests/__init__.py
0 → 100644
View file @
db30982b
#
lib/python/transaction/tests/abstestIDataManager.py
0 → 100644
View file @
db30982b
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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 cases for objects implementing IDataManager.
This is a combo test between Connection and DB, since the two are
rather incestuous and the DB Interface is not defined that I was
able to find.
To do a full test suite one would probably want to write a dummy
storage that will raise errors as needed for testing.
I started this test suite to reproduce a very simple error (tpc_abort
had an error and wouldn't even run if called). So it is *very*
incomplete, and even the tests that exist do not make sure that
the data actually gets written/not written to the storge.
Obviously this test suite should be expanded.
$Id: abstestIDataManager.py,v 1.5 2004/03/05 22:08:50 jim Exp $
"""
from
unittest
import
TestCase
from
transaction.interfaces
import
IRollback
class
IDataManagerTests
(
TestCase
,
object
):
def
setUp
(
self
):
self
.
datamgr
=
None
# subclass should override
self
.
obj
=
None
# subclass should define Persistent object
self
.
txn_factory
=
None
def
get_transaction
(
self
):
return
self
.
txn_factory
()
################################
# IDataManager interface tests #
################################
def
testCommitObj
(
self
):
tran
=
self
.
get_transaction
()
self
.
datamgr
.
prepare
(
tran
)
self
.
datamgr
.
commit
(
tran
)
def
testAbortTran
(
self
):
tran
=
self
.
get_transaction
()
self
.
datamgr
.
prepare
(
tran
)
self
.
datamgr
.
abort
(
tran
)
def
testRollback
(
self
):
tran
=
self
.
get_transaction
()
rb
=
self
.
datamgr
.
savepoint
(
tran
)
self
.
assert_
(
IRollback
.
providedBy
(
rb
))
lib/python/transaction/tests/test_SampleDataManager.py
0 → 100644
View file @
db30982b
##############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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: test_SampleDataManager.py,v 1.3 2004/04/19 21:19:11 tim_one Exp $
"""
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
()
lib/python/transaction/tests/test_register_compat.py
0 → 100644
View file @
db30982b
##############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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 backwards compatibility for resource managers using register().
The transaction package supports several different APIs for resource
managers. The original ZODB3 API was implemented by ZODB.Connection.
The Connection passed persistent objects to a Transaction's register()
method. It's possible that third-party code also used this API, hence
these tests that the code that adapts the old interface to the current
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
"""
import
transaction
class
Object
(
object
):
def
commit
(
self
):
pass
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
:
def
__init__
(
self
):
self
.
committed
=
[]
self
.
aborted
=
[]
self
.
calls
=
[]
def
register
(
self
,
obj
):
obj
.
_p_jar
=
self
transaction
.
get
().
register
(
obj
)
def
sortKey
(
self
):
return
str
(
id
(
self
))
def
tpc_begin
(
self
,
txn
,
sub
):
self
.
calls
.
append
(
"begin"
)
def
tpc_vote
(
self
,
txn
):
self
.
calls
.
append
(
"vote"
)
def
tpc_finish
(
self
,
txn
):
self
.
calls
.
append
(
"finish"
)
def
tpc_abort
(
self
,
txn
):
self
.
calls
.
append
(
"abort"
)
def
commit
(
self
,
obj
,
txn
):
obj
.
commit
()
self
.
committed
.
append
(
obj
)
def
abort
(
self
,
obj
,
txn
):
obj
.
abort
()
self
.
aborted
.
append
(
obj
)
import
doctest
def
test_suite
():
return
doctest
.
DocTestSuite
()
lib/python/transaction/tests/test_transaction.py
0 → 100644
View file @
db30982b
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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 tranasction 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.
$Id: test_transaction.py,v 1.2 2004/04/16 15:58:10 jeremy Exp $
"""
import
unittest
import
transaction
from
ZODB.utils
import
positive_id
class
TransactionTests
(
unittest
.
TestCase
):
def
setUp
(
self
):
self
.
orig_tm
=
transaction
.
manager
transaction
.
manager
=
transaction
.
TransactionManager
()
self
.
sub1
=
DataObject
()
self
.
sub2
=
DataObject
()
self
.
sub3
=
DataObject
()
self
.
nosub1
=
DataObject
(
nost
=
1
)
def
tearDown
(
self
):
transaction
.
manager
=
self
.
orig_tm
# 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
()
transaction
.
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
()
transaction
.
abort
()
assert
self
.
sub2
.
_p_jar
.
cabort
==
1
def
testTransactionNote
(
self
):
t
=
transaction
.
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.
\
n
\
n
Another.'
)
t
.
abort
()
def
testSubTransactionCommitCommit
(
self
):
self
.
sub1
.
modify
()
self
.
sub2
.
modify
()
transaction
.
commit
(
1
)
assert
self
.
sub1
.
_p_jar
.
ctpc_vote
==
0
assert
self
.
sub1
.
_p_jar
.
ctpc_finish
==
1
transaction
.
commit
()
assert
self
.
sub1
.
_p_jar
.
ccommit_sub
==
1
assert
self
.
sub1
.
_p_jar
.
ctpc_vote
==
1
def
testSubTransactionCommitAbort
(
self
):
self
.
sub1
.
modify
()
self
.
sub2
.
modify
()
transaction
.
commit
(
1
)
transaction
.
abort
()
assert
self
.
sub1
.
_p_jar
.
ctpc_vote
==
0
assert
self
.
sub1
.
_p_jar
.
cabort
==
0
assert
self
.
sub1
.
_p_jar
.
cabort_sub
==
1
def
testMultipleSubTransactionCommitCommit
(
self
):
self
.
sub1
.
modify
()
transaction
.
commit
(
1
)
self
.
sub2
.
modify
()
# reset a flag on the original to test it again
self
.
sub1
.
ctpc_finish
=
0
transaction
.
commit
(
1
)
# this is interesting.. we go through
# every subtrans commit with all subtrans capable
# objects... i don't like this but its an impl artifact
assert
self
.
sub1
.
_p_jar
.
ctpc_vote
==
0
assert
self
.
sub1
.
_p_jar
.
ctpc_finish
>
0
# add another before we do the entire txn commit
self
.
sub3
.
modify
()
transaction
.
commit
()
# we did an implicit sub commit, is this impl artifact?
assert
self
.
sub3
.
_p_jar
.
ccommit_sub
==
1
assert
self
.
sub1
.
_p_jar
.
ctpc_finish
>
1
def
testMultipleSubTransactionCommitAbortSub
(
self
):
"""
sub1 calling method commit
sub1 calling method tpc_finish
sub2 calling method tpc_begin
sub2 calling method commit
sub2 calling method tpc_finish
sub3 calling method abort
sub1 calling method commit_sub
sub2 calling method commit_sub
sub2 calling method tpc_vote
sub1 calling method tpc_vote
sub1 calling method tpc_finish
sub2 calling method tpc_finish
"""
# add it
self
.
sub1
.
modify
()
transaction
.
commit
(
1
)
# add another
self
.
sub2
.
modify
()
transaction
.
commit
(
1
)
assert
self
.
sub1
.
_p_jar
.
ctpc_vote
==
0
assert
self
.
sub1
.
_p_jar
.
ctpc_finish
>
0
# add another before we do the entire txn commit
self
.
sub3
.
modify
()
# abort the sub transaction
transaction
.
abort
(
1
)
# commit the container transaction
transaction
.
commit
()
assert
self
.
sub3
.
_p_jar
.
cabort
==
1
assert
self
.
sub1
.
_p_jar
.
ccommit_sub
==
1
assert
self
.
sub1
.
_p_jar
.
ctpc_finish
>
1
# repeat adding in a nonsub trans jars
def
testNSJTransactionCommit
(
self
):
self
.
nosub1
.
modify
()
transaction
.
commit
()
assert
self
.
nosub1
.
_p_jar
.
ctpc_finish
==
1
def
testNSJTransactionAbort
(
self
):
self
.
nosub1
.
modify
()
transaction
.
abort
()
assert
self
.
nosub1
.
_p_jar
.
ctpc_finish
==
0
assert
self
.
nosub1
.
_p_jar
.
cabort
==
1
# XXX
def
BUGtestNSJSubTransactionCommitAbort
(
self
):
"""
this reveals a bug in transaction.py
the nosub jar should not have tpc_finish
called on it till the containing txn
ends.
sub calling method commit
nosub calling method tpc_begin
sub calling method tpc_finish
nosub calling method tpc_finish
nosub calling method abort
sub calling method abort_sub
"""
self
.
sub1
.
modify
(
tracing
=
'sub'
)
self
.
nosub1
.
modify
(
tracing
=
'nosub'
)
transaction
.
commit
(
1
)
assert
self
.
sub1
.
_p_jar
.
ctpc_finish
==
1
# bug, non sub trans jars are getting finished
# in a subtrans
assert
self
.
nosub1
.
_p_jar
.
ctpc_finish
==
0
transaction
.
abort
()
assert
self
.
nosub1
.
_p_jar
.
cabort
==
1
assert
self
.
sub1
.
_p_jar
.
cabort_sub
==
1
def
testNSJSubTransactionCommitCommit
(
self
):
self
.
sub1
.
modify
()
self
.
nosub1
.
modify
()
transaction
.
commit
(
1
)
assert
self
.
nosub1
.
_p_jar
.
ctpc_vote
==
0
transaction
.
commit
()
#assert self.nosub1._p_jar.ccommit_sub == 0
assert
self
.
nosub1
.
_p_jar
.
ctpc_vote
==
1
assert
self
.
sub1
.
_p_jar
.
ccommit_sub
==
1
assert
self
.
sub1
.
_p_jar
.
ctpc_vote
==
1
def
testNSJMultipleSubTransactionCommitCommit
(
self
):
"""
sub1 calling method tpc_begin
sub1 calling method commit
sub1 calling method tpc_finish
nosub calling method tpc_begin
nosub calling method tpc_finish
sub2 calling method tpc_begin
sub2 calling method commit
sub2 calling method tpc_finish
nosub calling method tpc_begin
nosub calling method commit
sub1 calling method commit_sub
sub2 calling method commit_sub
sub1 calling method tpc_vote
nosub calling method tpc_vote
sub2 calling method tpc_vote
sub2 calling method tpc_finish
nosub calling method tpc_finish
sub1 calling method tpc_finish
"""
# add it
self
.
sub1
.
modify
()
transaction
.
commit
(
1
)
# add another
self
.
nosub1
.
modify
()
transaction
.
commit
(
1
)
assert
self
.
sub1
.
_p_jar
.
ctpc_vote
==
0
assert
self
.
nosub1
.
_p_jar
.
ctpc_vote
==
0
assert
self
.
sub1
.
_p_jar
.
ctpc_finish
>
0
# add another before we do the entire txn commit
self
.
sub2
.
modify
()
# commit the container transaction
transaction
.
commit
()
# we did an implicit sub commit
assert
self
.
sub2
.
_p_jar
.
ccommit_sub
==
1
assert
self
.
sub1
.
_p_jar
.
ctpc_finish
>
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
=
SubTransactionJar
(
errors
=
'abort'
)
self
.
nosub1
.
modify
()
self
.
sub1
.
modify
(
nojar
=
1
)
self
.
sub2
.
modify
()
try
:
transaction
.
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
=
SubTransactionJar
(
errors
=
'commit'
)
self
.
nosub1
.
modify
()
self
.
sub1
.
modify
(
nojar
=
1
)
try
:
transaction
.
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
=
SubTransactionJar
(
errors
=
'tpc_vote'
)
self
.
nosub1
.
modify
()
self
.
sub1
.
modify
(
nojar
=
1
)
try
:
transaction
.
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
=
SubTransactionJar
(
errors
=
'tpc_begin'
)
self
.
nosub1
.
modify
()
self
.
sub1
.
modify
(
nojar
=
1
)
try
:
transaction
.
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
=
SubTransactionJar
(
errors
=
(
'tpc_abort'
,
'tpc_vote'
))
self
.
nosub1
.
modify
()
self
.
sub1
.
modify
(
nojar
=
1
)
try
:
transaction
.
commit
()
except
TestTxnException
:
pass
assert
self
.
nosub1
.
_p_jar
.
ctpc_abort
==
1
### More Failure modes...
# now we mix in some sub transactions
###
def
testExceptionInSubCommitSub
(
self
):
# It's harder than normal to verify test results, because
# the subtransaction jars are stored in a dictionary. The
# order in which jars are processed depends on the order
# they come out of the dictionary.
self
.
sub1
.
modify
()
transaction
.
commit
(
1
)
self
.
nosub1
.
modify
()
self
.
sub2
.
_p_jar
=
SubTransactionJar
(
errors
=
'commit_sub'
)
self
.
sub2
.
modify
(
nojar
=
1
)
transaction
.
commit
(
1
)
self
.
sub3
.
modify
()
try
:
transaction
.
commit
()
except
TestTxnException
:
pass
if
self
.
sub1
.
_p_jar
.
ccommit_sub
:
self
.
assertEqual
(
self
.
sub1
.
_p_jar
.
ctpc_abort
,
1
)
else
:
self
.
assertEqual
(
self
.
sub1
.
_p_jar
.
cabort_sub
,
1
)
self
.
assertEqual
(
self
.
sub2
.
_p_jar
.
ctpc_abort
,
1
)
self
.
assertEqual
(
self
.
nosub1
.
_p_jar
.
ctpc_abort
,
1
)
if
self
.
sub3
.
_p_jar
.
ccommit_sub
:
self
.
assertEqual
(
self
.
sub3
.
_p_jar
.
ctpc_abort
,
1
)
else
:
self
.
assertEqual
(
self
.
sub3
.
_p_jar
.
cabort_sub
,
1
)
def
testExceptionInSubAbortSub
(
self
):
# This test has two errors. When commit_sub() is called on
# sub1, it will fail. If sub1 is handled first, it will raise
# an except and abort_sub() will be called on sub2. If sub2
# is handled first, then commit_sub() will fail after sub2 has
# already begun its top-level transaction and tpc_abort() will
# be called.
self
.
sub1
.
_p_jar
=
SubTransactionJar
(
errors
=
'commit_sub'
)
self
.
sub1
.
modify
(
nojar
=
1
)
transaction
.
commit
(
1
)
self
.
nosub1
.
modify
()
self
.
sub2
.
_p_jar
=
SubTransactionJar
(
errors
=
'abort_sub'
)
self
.
sub2
.
modify
(
nojar
=
1
)
transaction
.
commit
(
1
)
self
.
sub3
.
modify
()
try
:
transaction
.
commit
()
except
TestTxnException
,
err
:
pass
else
:
self
.
fail
(
"expected transaction to fail"
)
# The last commit failed. If the commit_sub() method was
# called, then tpc_abort() should be called to abort the
# actual transaction. If not, then calling abort_sub() is
# sufficient.
if
self
.
sub3
.
_p_jar
.
ccommit_sub
:
self
.
assertEqual
(
self
.
sub3
.
_p_jar
.
ctpc_abort
,
1
)
else
:
self
.
assertEqual
(
self
.
sub3
.
_p_jar
.
cabort_sub
,
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
DataObject
:
def
__init__
(
self
,
nost
=
0
):
self
.
nost
=
nost
self
.
_p_jar
=
None
def
modify
(
self
,
nojar
=
0
,
tracing
=
0
):
if
not
nojar
:
if
self
.
nost
:
self
.
_p_jar
=
NoSubTransactionJar
(
tracing
=
tracing
)
else
:
self
.
_p_jar
=
SubTransactionJar
(
tracing
=
tracing
)
transaction
.
get
().
register
(
self
)
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
SubTransactionJar
(
BasicJar
):
def
abort_sub
(
self
,
txn
):
self
.
check
(
'abort_sub'
)
self
.
cabort_sub
=
1
def
commit_sub
(
self
,
txn
):
self
.
check
(
'commit_sub'
)
self
.
ccommit_sub
=
1
class
NoSubTransactionJar
(
BasicJar
):
pass
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 ZODB.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
test_suite
():
from
doctest
import
DocTestSuite
return
unittest
.
TestSuite
((
DocTestSuite
(),
unittest
.
makeSuite
(
TransactionTests
),
))
if
__name__
==
'__main__'
:
unittest
.
TextTestRunner
().
run
(
test_suite
())
lib/python/transaction/tests/test_util.py
0 → 100644
View file @
db30982b
##############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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 utilities
$Id: test_util.py,v 1.2 2004/02/20 16:56:58 fdrake Exp $
"""
import
unittest
from
doctest
import
DocTestSuite
def
test_suite
():
return
DocTestSuite
(
'transaction.util'
)
if
__name__
==
'__main__'
:
unittest
.
main
(
defaultTest
=
'test_suite'
)
lib/python/transaction/util.py
0 → 100644
View file @
db30982b
##############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
##############################################################################
"""Utility classes or functions
$Id: util.py,v 1.3 2004/04/19 21:19:10 tim_one Exp $
"""
from
transaction.interfaces
import
IRollback
try
:
from
zope.interface
import
implements
except
ImportError
:
def
implements
(
*
args
):
pass
class
NoSavepointSupportRollback
:
"""Rollback for data managers that don't support savepoints
>>> class DataManager:
... def savepoint(self, txn):
... return NoSavepointSupportRollback(self)
>>> rb = DataManager().savepoint('some transaction')
>>> rb.rollback()
Traceback (most recent call last):
...
NotImplementedError: """
\
"""DataManager data managers do not support """
\
"""savepoints (aka subtransactions
"""
implements
(
IRollback
)
def
__init__
(
self
,
dm
):
self
.
dm
=
dm
.
__class__
.
__name__
def
rollback
(
self
):
raise
NotImplementedError
(
"%s data managers do not support savepoints (aka subtransactions"
%
self
.
dm
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment