Commit d0a5048d authored by Jim Fulton's avatar Jim Fulton

Make transactions uncommitable if savepoint rollback fails.

Added demonstration of transaction non-commitability after savepoint
or savepoint rollback failure.

Updated "previous commit failed" error to "previous operation failed".
parent a8857477
......@@ -231,17 +231,17 @@ class Transaction(object):
# Raise TransactionFailedError, due to commit()/join()/register()
# getting called when the current transaction has already suffered
# a commit failure.
def _prior_commit_failed(self):
# a commit/savepoint failure.
def _prior_operation_failed(self):
from ZODB.POSException import TransactionFailedError
assert self._failure_traceback is not None
raise TransactionFailedError("commit() previously failed, "
"with this traceback:\n\n%s" %
raise TransactionFailedError("An operation previously failed, "
"with traceback:\n\n%s" %
self._failure_traceback.getvalue())
def join(self, resource):
if self.status is Status.COMMITFAILED:
self._prior_commit_failed() # doesn't return
self._prior_operation_failed() # doesn't return
if self.status is not Status.ACTIVE:
# TODO: Should it be possible to join a committing transaction?
......@@ -261,15 +261,15 @@ class Transaction(object):
def savepoint(self, optimistic=False):
if self.status is Status.COMMITFAILED:
self._prior_commit_failed() # doesn't return, it raises
self._prior_operation_failed() # doesn't return, it raises
try:
savepoint = Savepoint(optimistic)
savepoint = Savepoint(self, optimistic)
for resource in self._resources:
savepoint.join(resource)
except:
self._cleanup(self._resources)
self._saveCommitishError() # doesn't return, it raises!
self._saveCommitishError() # reraises!
if self._last_savepoint is not None:
savepoint.previous = self._last_savepoint
......@@ -330,7 +330,7 @@ class Transaction(object):
return
if self.status is Status.COMMITFAILED:
self._prior_commit_failed() # doesn't return
self._prior_operation_failed() # doesn't return
self._callBeforeCommitHooks()
......@@ -598,7 +598,8 @@ class Savepoint:
"""
interface.implements(interfaces.ISavepoint)
def __init__(self, optimistic):
def __init__(self, transaction, optimistic):
self.transaction = transaction
self._savepoints = []
self.valid = True
self.next = self.previous = None
......@@ -620,8 +621,12 @@ class Savepoint:
if not self.valid:
raise interfaces.InvalidSavepointRollbackError
self._invalidate_next()
for savepoint in self._savepoints:
savepoint.rollback()
try:
for savepoint in self._savepoints:
savepoint.rollback()
except:
# Mark the transaction as failed
self.transaction._saveCommitishError() # reraises!
def _invalidate_next(self):
self.valid = False
......
......@@ -219,3 +219,57 @@ is there are no reasons to roll back:
...
TypeError: ('Savepoints unsupported', {'name': 'sam'})
Failures
--------
If a failure occurs when creating or rolling back a savepoint, the
transaction state will be uncertain and the transaction will become
uncommitable. From that point on, most transaction operations,
including commit, will fail until the transaction is aborted.
In the previous example, we got an error when we tried to rollback the
savepoint. If we try to commit the transaction, the commit will fail:
>>> transaction.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TransactionFailedError: An operation previously failed, with traceback:
...
TypeError: ('Savepoints unsupported', {'name': 'sam'})
<BLANKLINE>
We have to abort it to make any progress:
>>> transaction.abort()
Similarly, in our earlier example, where we tried to take a savepoint
with a data manager that didn't support savepoints:
>>> dm_no_sp['name'] = 'sally'
>>> dm['name'] = 'sally'
>>> savepoint = transaction.savepoint()
Traceback (most recent call last):
...
TypeError: ('Savepoints unsupported', {'name': 'sue'})
>>> transaction.commit() # doctest: +ELLIPSIS
Traceback (most recent call last):
...
TransactionFailedError: An operation previously failed, with traceback:
...
TypeError: ('Savepoints unsupported', {'name': 'sue'})
<BLANKLINE>
>>> transaction.abort()
After clearing the transaction with an abort, we can get on with new
transactions:
>>> dm_no_sp['name'] = 'sally'
>>> dm['name'] = 'sally'
>>> transaction.commit()
>>> dm_no_sp['name']
'sally'
>>> dm['name']
'sally'
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