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): ...@@ -231,17 +231,17 @@ class Transaction(object):
# 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 failure. # a commit/savepoint failure.
def _prior_commit_failed(self): def _prior_operation_failed(self):
from ZODB.POSException import TransactionFailedError from ZODB.POSException import TransactionFailedError
assert self._failure_traceback is not None assert self._failure_traceback is not None
raise TransactionFailedError("commit() previously failed, " raise TransactionFailedError("An operation previously failed, "
"with this traceback:\n\n%s" % "with traceback:\n\n%s" %
self._failure_traceback.getvalue()) self._failure_traceback.getvalue())
def join(self, resource): def join(self, resource):
if self.status is Status.COMMITFAILED: 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: if self.status is not Status.ACTIVE:
# TODO: Should it be possible to join a committing transaction? # TODO: Should it be possible to join a committing transaction?
...@@ -261,15 +261,15 @@ class Transaction(object): ...@@ -261,15 +261,15 @@ class Transaction(object):
def savepoint(self, optimistic=False): def savepoint(self, optimistic=False):
if self.status is Status.COMMITFAILED: 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: try:
savepoint = Savepoint(optimistic) savepoint = Savepoint(self, optimistic)
for resource in self._resources: for resource in self._resources:
savepoint.join(resource) savepoint.join(resource)
except: except:
self._cleanup(self._resources) self._cleanup(self._resources)
self._saveCommitishError() # doesn't return, it raises! self._saveCommitishError() # reraises!
if self._last_savepoint is not None: if self._last_savepoint is not None:
savepoint.previous = self._last_savepoint savepoint.previous = self._last_savepoint
...@@ -330,7 +330,7 @@ class Transaction(object): ...@@ -330,7 +330,7 @@ class Transaction(object):
return return
if self.status is Status.COMMITFAILED: if self.status is Status.COMMITFAILED:
self._prior_commit_failed() # doesn't return self._prior_operation_failed() # doesn't return
self._callBeforeCommitHooks() self._callBeforeCommitHooks()
...@@ -598,7 +598,8 @@ class Savepoint: ...@@ -598,7 +598,8 @@ class Savepoint:
""" """
interface.implements(interfaces.ISavepoint) interface.implements(interfaces.ISavepoint)
def __init__(self, optimistic): def __init__(self, transaction, optimistic):
self.transaction = transaction
self._savepoints = [] self._savepoints = []
self.valid = True self.valid = True
self.next = self.previous = None self.next = self.previous = None
...@@ -620,8 +621,12 @@ class Savepoint: ...@@ -620,8 +621,12 @@ class Savepoint:
if not self.valid: if not self.valid:
raise interfaces.InvalidSavepointRollbackError raise interfaces.InvalidSavepointRollbackError
self._invalidate_next() self._invalidate_next()
for savepoint in self._savepoints: try:
savepoint.rollback() for savepoint in self._savepoints:
savepoint.rollback()
except:
# Mark the transaction as failed
self.transaction._saveCommitishError() # reraises!
def _invalidate_next(self): def _invalidate_next(self):
self.valid = False self.valid = False
......
...@@ -219,3 +219,57 @@ is there are no reasons to roll back: ...@@ -219,3 +219,57 @@ is there are no reasons to roll back:
... ...
TypeError: ('Savepoints unsupported', {'name': 'sam'}) 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