Commit 10ec1cb2 authored by Brian Sutherland's avatar Brian Sutherland

merge 70049:70059 from the jinty-doom branch.

These are the ZODB part of the patches I posted to
http://www.zope.org/Collectors/Zope3-dev/655 with an additional import cleanup.
The issue was silent for over 2 weeks.

Also my attempts at subscribing to zodb-dev or sending a message there
dissapeared were doomed;)

I'm fully willing to revert this if someone has an issue with it.

    ------------------------------------------------------------------------
    r70059 | jinty | 2006-09-08 16:26:44 +0200 (Fri, 08 Sep 2006) | 1 line

    Forgot the NEWS entry. Hope I did it right.
    ------------------------------------------------------------------------
    r70053 | jinty | 2006-09-08 14:43:26 +0200 (Fri, 08 Sep 2006) | 1 line

    Clean up wierd import dance with ZODB. This is unnecessary since the transaction module stopped being imported in ZODB/__init__.py in rev 39622.
    ------------------------------------------------------------------------
    r70051 | jinty | 2006-09-08 13:24:45 +0200 (Fri, 08 Sep 2006) | 1 line

    Add the ability to ask a transaction if it has been doomed i.e. isDoomed().
    ------------------------------------------------------------------------
    r70050 | jinty | 2006-09-08 13:13:06 +0200 (Fri, 08 Sep 2006) | 1 line

    Add the doom() function to transactions. Look at tests/doom.txt for more info.
    ------------------------------------------------------------------------

parent 44134205
What's new on ZODB 3.8a1?
=========================
Transactions
------------
- (3.8a1) Add a doom() and isDoomed() interface to the transaction module.
First step towards the resolution of
http://www.zope.org/Collectors/Zope3-dev/655
A doomed transaction behaves exactly the same way as an active transaction
but raises an error on any attempt to commit it, thus forcing an abort.
Doom is useful in places where abort is unsafe and an exception cannot be
raised. This occurs when the programmer wants the code following the doom to
run but not commit. It is unsafe to abort in these circumstances as a
following get() may implicitly open a new transaction.
Any attempt to commit a doomed transaction will raise a DoomedTransaction
exception.
- (3.8a1) Clean up the ZODB imports in transaction.
Clean up weird import dance with ZODB. This is unnecessary since the
transaction module stopped being imported in ZODB/__init__.py in rev 39622.
What's new on ZODB 3.7b2? What's new on ZODB 3.7b2?
========================= =========================
......
...@@ -24,4 +24,6 @@ get = manager.get ...@@ -24,4 +24,6 @@ get = manager.get
begin = manager.begin begin = manager.begin
commit = manager.commit commit = manager.commit
abort = manager.abort abort = manager.abort
doom = manager.doom
isDoomed = manager.isDoomed
savepoint = manager.savepoint savepoint = manager.savepoint
...@@ -19,9 +19,11 @@ are associated with the right transaction. ...@@ -19,9 +19,11 @@ are associated with the right transaction.
import thread import thread
from ZODB.utils import WeakSet, deprecated37
from transaction._transaction import Transaction from transaction._transaction import Transaction
# Used for deprecated arguments. ZODB.utils.DEPRECATED_ARGUMENT is # Used for deprecated arguments. ZODB.utils.DEPRECATED_ARGUMENT was
# too hard to use here, due to the convoluted import dance across # too hard to use here, due to the convoluted import dance across
# __init__.py files. # __init__.py files.
_marker = object() _marker = object()
...@@ -34,8 +36,6 @@ _marker = object() ...@@ -34,8 +36,6 @@ _marker = object()
# alive (e.g., the cache, and everything reachable from it too). # alive (e.g., the cache, and everything reachable from it too).
# Therefore we use "weak sets" internally. # Therefore we use "weak sets" internally.
# #
# Obscure: because of the __init__.py maze, we can't import WeakSet
# at top level here.
# Call the ISynchronizer newTransaction() method on every element of # Call the ISynchronizer newTransaction() method on every element of
# WeakSet synchs. # WeakSet synchs.
...@@ -58,8 +58,6 @@ def _new_transaction(txn, synchs): ...@@ -58,8 +58,6 @@ def _new_transaction(txn, synchs):
class TransactionManager(object): class TransactionManager(object):
def __init__(self): def __init__(self):
from ZODB.utils import WeakSet
self._txn = None self._txn = None
self._synchs = WeakSet() self._synchs = WeakSet()
...@@ -85,11 +83,16 @@ class TransactionManager(object): ...@@ -85,11 +83,16 @@ class TransactionManager(object):
def unregisterSynch(self, synch): def unregisterSynch(self, synch):
self._synchs.remove(synch) self._synchs.remove(synch)
def isDoomed(self):
return self.get().isDoomed()
def doom(self):
return self.get().doom()
def commit(self, sub=_marker): def commit(self, sub=_marker):
if sub is _marker: if sub is _marker:
sub = None sub = None
else: else:
from ZODB.utils import deprecated37
deprecated37("subtransactions are deprecated; use " deprecated37("subtransactions are deprecated; use "
"transaction.savepoint() instead of " "transaction.savepoint() instead of "
"transaction.commit(1)") "transaction.commit(1)")
...@@ -99,7 +102,6 @@ class TransactionManager(object): ...@@ -99,7 +102,6 @@ class TransactionManager(object):
if sub is _marker: if sub is _marker:
sub = None sub = None
else: else:
from ZODB.utils import deprecated37
deprecated37("subtransactions are deprecated; use " deprecated37("subtransactions are deprecated; use "
"sp.rollback() instead of " "sp.rollback() instead of "
"transaction.abort(1), where `sp` is the " "transaction.abort(1), where `sp` is the "
...@@ -132,7 +134,6 @@ class ThreadTransactionManager(TransactionManager): ...@@ -132,7 +134,6 @@ class ThreadTransactionManager(TransactionManager):
synchs = self._synchs.get(tid) synchs = self._synchs.get(tid)
if synchs is None: if synchs is None:
from ZODB.utils import WeakSet
synchs = self._synchs[tid] = WeakSet() synchs = self._synchs[tid] = WeakSet()
txn = self._txns[tid] = Transaction(synchs, self) txn = self._txns[tid] = Transaction(synchs, self)
...@@ -145,7 +146,6 @@ class ThreadTransactionManager(TransactionManager): ...@@ -145,7 +146,6 @@ class ThreadTransactionManager(TransactionManager):
if txn is None: if txn is None:
synchs = self._synchs.get(tid) synchs = self._synchs.get(tid)
if synchs is None: if synchs is None:
from ZODB.utils import WeakSet
synchs = self._synchs[tid] = WeakSet() synchs = self._synchs[tid] = WeakSet()
txn = self._txns[tid] = Transaction(synchs, self) txn = self._txns[tid] = Transaction(synchs, self)
return txn return txn
...@@ -159,7 +159,6 @@ class ThreadTransactionManager(TransactionManager): ...@@ -159,7 +159,6 @@ class ThreadTransactionManager(TransactionManager):
tid = thread.get_ident() tid = thread.get_ident()
ws = self._synchs.get(tid) ws = self._synchs.get(tid)
if ws is None: if ws is None:
from ZODB.utils import WeakSet
ws = self._synchs[tid] = WeakSet() ws = self._synchs[tid] = WeakSet()
ws.add(synch) ws.add(synch)
......
...@@ -169,15 +169,13 @@ import traceback ...@@ -169,15 +169,13 @@ import traceback
from cStringIO import StringIO from cStringIO import StringIO
from zope import interface from zope import interface
from transaction import interfaces from ZODB.utils import WeakSet
from ZODB.utils import deprecated37, deprecated38
from ZODB.POSException import TransactionFailedError
from ZODB.utils import oid_repr
# Sigh. In the maze of __init__.py's, ZODB.__init__.py takes 'get' from transaction import interfaces
# out of transaction.__init__.py, in order to stuff the 'get_transaction'
# alias in __builtin__. So here in _transaction.py, we can't import
# exceptions from ZODB.POSException at top level (we're imported by
# our __init__.py, which is imported by ZODB's __init__, so the ZODB
# package isn't well-formed when we're first imported).
# from ZODB.POSException import TransactionError, TransactionFailedError
_marker = object() _marker = object()
...@@ -193,6 +191,8 @@ class Status: ...@@ -193,6 +191,8 @@ class Status:
COMMITTING = "Committing" COMMITTING = "Committing"
COMMITTED = "Committed" COMMITTED = "Committed"
DOOMED = "Doomed"
# commit() or commit(True) raised an exception. All further attempts # commit() or commit(True) raised an exception. All further attempts
# to commit or join this transaction will raise TransactionFailedError. # to commit or join this transaction will raise TransactionFailedError.
COMMITFAILED = "Commit failed" COMMITFAILED = "Commit failed"
...@@ -227,7 +227,6 @@ class Transaction(object): ...@@ -227,7 +227,6 @@ class Transaction(object):
# Weak set of synchronizer objects to call. # Weak set of synchronizer objects to call.
if synchronizers is None: if synchronizers is None:
from ZODB.utils import WeakSet
synchronizers = WeakSet() synchronizers = WeakSet()
self._synchronizers = synchronizers self._synchronizers = synchronizers
...@@ -258,11 +257,21 @@ class Transaction(object): ...@@ -258,11 +257,21 @@ class Transaction(object):
# List of (hook, args, kws) tuples added by addAfterCommitHook(). # List of (hook, args, kws) tuples added by addAfterCommitHook().
self._after_commit = [] self._after_commit = []
def isDoomed(self):
return self.status is Status.DOOMED
def doom(self):
if self.status is not Status.DOOMED:
if self.status is not Status.ACTIVE:
# should not doom transactions in the middle,
# or after, a commit
raise AssertionError()
self.status = Status.DOOMED
# Raise TransactionFailedError, due to commit()/join()/register() # Raise TransactionFailedError, due to commit()/join()/register()
# getting called when the current transaction has already suffered # getting called when the current transaction has already suffered
# a commit/savepoint failure. # a commit/savepoint failure.
def _prior_operation_failed(self): def _prior_operation_failed(self):
from ZODB.POSException import TransactionFailedError
assert self._failure_traceback is not None assert self._failure_traceback is not None
raise TransactionFailedError("An operation previously failed, " raise TransactionFailedError("An operation previously failed, "
"with traceback:\n\n%s" % "with traceback:\n\n%s" %
...@@ -272,11 +281,12 @@ class Transaction(object): ...@@ -272,11 +281,12 @@ class Transaction(object):
if self.status is Status.COMMITFAILED: if self.status is Status.COMMITFAILED:
self._prior_operation_failed() # doesn't return self._prior_operation_failed() # doesn't return
if self.status is not Status.ACTIVE: if (self.status is not Status.ACTIVE and
self.status is not Status.DOOMED):
# TODO: Should it be possible to join a committing transaction? # TODO: Should it be possible to join a committing transaction?
# I think some users want it. # I think some users want it.
raise ValueError("expected txn status %r, but it's %r" % ( raise ValueError("expected txn status %r or %r, but it's %r" % (
Status.ACTIVE, self.status)) Status.ACTIVE, Status.DOOMED, self.status))
# TODO: the prepare check is a bit of a hack, perhaps it would # TODO: the prepare check is a bit of a hack, perhaps it would
# be better to use interfaces. If this is a ZODB4-style # be better to use interfaces. If this is a ZODB4-style
# resource manager, it needs to be adapted, too. # resource manager, it needs to be adapted, too.
...@@ -363,10 +373,12 @@ class Transaction(object): ...@@ -363,10 +373,12 @@ class Transaction(object):
adapter.objects.append(obj) adapter.objects.append(obj)
def commit(self, subtransaction=_marker, deprecation_wng=True): def commit(self, subtransaction=_marker, deprecation_wng=True):
if self.status is Status.DOOMED:
raise interfaces.DoomedTransaction()
if subtransaction is _marker: if subtransaction is _marker:
subtransaction = 0 subtransaction = 0
elif deprecation_wng: elif deprecation_wng:
from ZODB.utils import deprecated37
deprecated37("subtransactions are deprecated; instead of " deprecated37("subtransactions are deprecated; instead of "
"transaction.commit(1), use " "transaction.commit(1), use "
"transaction.savepoint(optimistic=True) in " "transaction.savepoint(optimistic=True) in "
...@@ -431,7 +443,6 @@ class Transaction(object): ...@@ -431,7 +443,6 @@ class Transaction(object):
self._before_commit.append((hook, tuple(args), kws)) self._before_commit.append((hook, tuple(args), kws))
def beforeCommitHook(self, hook, *args, **kws): def beforeCommitHook(self, hook, *args, **kws):
from ZODB.utils import deprecated38
deprecated38("Use addBeforeCommitHook instead of beforeCommitHook.") deprecated38("Use addBeforeCommitHook instead of beforeCommitHook.")
self.addBeforeCommitHook(hook, args, kws) self.addBeforeCommitHook(hook, args, kws)
...@@ -539,7 +550,6 @@ class Transaction(object): ...@@ -539,7 +550,6 @@ class Transaction(object):
if subtransaction is _marker: if subtransaction is _marker:
subtransaction = 0 subtransaction = 0
elif deprecation_wng: elif deprecation_wng:
from ZODB.utils import deprecated37
deprecated37("subtransactions are deprecated; use " deprecated37("subtransactions are deprecated; use "
"sp.rollback() instead of " "sp.rollback() instead of "
"transaction.abort(1), where `sp` is the " "transaction.abort(1), where `sp` is the "
...@@ -659,8 +669,6 @@ def object_hint(o): ...@@ -659,8 +669,6 @@ def object_hint(o):
This function does not raise an exception. This function does not raise an exception.
""" """
from ZODB.utils import oid_repr
# We should always be able to get __class__. # We should always be able to get __class__.
klass = o.__class__.__name__ klass = o.__class__.__name__
# oid would be great, but may this isn't a persistent object. # oid would be great, but may this isn't a persistent object.
......
...@@ -45,6 +45,14 @@ class ITransactionManager(zope.interface.Interface): ...@@ -45,6 +45,14 @@ class ITransactionManager(zope.interface.Interface):
"""Abort the current transaction. """Abort the current transaction.
""" """
def doom():
"""Doom the current transaction.
"""
def isDoomed():
"""Returns True if the current transaction is doomed, otherwise False.
"""
def savepoint(optimistic=False): def savepoint(optimistic=False):
"""Create a savepoint from the current transaction. """Create a savepoint from the current transaction.
...@@ -115,6 +123,16 @@ class ITransaction(zope.interface.Interface): ...@@ -115,6 +123,16 @@ class ITransaction(zope.interface.Interface):
This is called from the application. This can only be called This is called from the application. This can only be called
before the two-phase commit protocol has been started. before the two-phase commit protocol has been started.
""" """
def doom():
"""Doom the transaction.
Dooms the current transaction. This will cause
DoomedTransactionException to be raised on any attempt to commit the
transaction.
Otherwise the transaction will behave as if it was active.
"""
def savepoint(optimistic=False): def savepoint(optimistic=False):
"""Create a savepoint. """Create a savepoint.
...@@ -453,3 +471,6 @@ class ISynchronizer(zope.interface.Interface): ...@@ -453,3 +471,6 @@ class ISynchronizer(zope.interface.Interface):
This hook is called when, and only when, a transaction manager's This hook is called when, and only when, a transaction manager's
begin() method is called explictly. begin() method is called explictly.
""" """
class DoomedTransaction(Exception):
"""A commit was attempted on a transaction that was doomed."""
Dooming Transactions
====================
A doomed transaction behaves exactly the same way as an active transaction but
raises an error on any attempt to commit it, thus forcing an abort.
Doom is useful in places where abort is unsafe and an exception cannot be
raised. This occurs when the programmer wants the code following the doom to
run but not commit. It is unsafe to abort in these circumstances as a following
get() may implicitly open a new transaction.
Any attempt to commit a doomed transaction will raise a DoomedTransaction
exception.
An example of such a use case can be found in
zope/app/form/browser/editview.py. Here a form validation failure must doom
the transaction as committing the transaction may have side-effects. However,
the form code must continue to calculate a form containing the error messages
to return.
For Zope in general, code running within a request should always doom
transactions rather than aborting them. It is the responsibilty of the
publication to either abort() or commit() the transaction. Application code can
use savepoints and doom() safely.
To see how it works we first need to create a stub data manager:
>>> from transaction.interfaces import IDataManager
>>> from zope.interface import implements
>>> class DataManager:
... implements(IDataManager)
... def __init__(self):
... self.attr_counter = {}
... def __getattr__(self, name):
... def f(transaction):
... self.attr_counter[name] = self.attr_counter.get(name, 0) + 1
... return f
... def total(self):
... count = 0
... for access_count in self.attr_counter.values():
... count += access_count
... return count
... def sortKey(self):
... return 1
Start a new transaction:
>>> import transaction
>>> txn = transaction.begin()
>>> dm = DataManager()
>>> txn.join(dm)
We can ask a transaction if it is doomed to avoid expensive operations. An
example of a use case is an object-relational mapper where a pre-commit hook
sends all outstanding SQL to a relational database for objects changed during
the transaction. This expensive operation is not necessary if the transaction
has been doomed. A non-doomed transaction should return False:
>>> txn.isDoomed()
False
We can doom a transaction by calling .doom() on it:
>>> txn.doom()
>>> txn.isDoomed()
True
We can doom it again if we like:
>>> txn.doom()
The data manager is unchanged at this point:
>>> dm.total()
0
Attempting to commit a doomed transaction any number of times raises a
DoomedTransaction:
>>> txn.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
DoomedTransaction
>>> txn.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
DoomedTransaction
But still leaves the data manager unchanged:
>>> dm.total()
0
But the doomed transaction can be aborted:
>>> txn.abort()
Which aborts the data manager:
>>> dm.total()
1
>>> dm.attr_counter['abort']
1
Dooming the current transaction can also be done directly from the transaction
module. We can also begin a new transaction directly after dooming the old one:
>>> txn = transaction.begin()
>>> transaction.isDoomed()
False
>>> transaction.doom()
>>> transaction.isDoomed()
True
>>> txn = transaction.begin()
After committing a transaction we get an assertion error if we try to doom the
transaction. This could be made more specific, but trying to doom a transaction
after it's been committed is probably a programming error:
>>> txn = transaction.begin()
>>> txn.commit()
>>> txn.doom()
Traceback (most recent call last):
...
AssertionError
A doomed transaction should act the same as an active transaction, so we should
be able to join it:
>>> txn = transaction.begin()
>>> txn.doom()
>>> dm2 = DataManager()
>>> txn.join(dm2)
Clean up:
>>> txn = transaction.begin()
>>> txn.abort()
...@@ -992,8 +992,9 @@ def test_addAfterCommitHook(): ...@@ -992,8 +992,9 @@ def test_addAfterCommitHook():
""" """
def test_suite(): def test_suite():
from zope.testing.doctest import DocTestSuite from zope.testing.doctest import DocTestSuite, DocFileSuite
return unittest.TestSuite(( return unittest.TestSuite((
DocFileSuite('doom.txt'),
DocTestSuite(), DocTestSuite(),
unittest.makeSuite(TransactionTests), unittest.makeSuite(TransactionTests),
)) ))
......
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