Commit 263b125e authored by Shane Hathaway's avatar Shane Hathaway

Fixed multiple undo in a transaction.

For multiple undo to succeed, the undo operations have to be
performed in a specific order.  The order was not being retained
by ZODB and was instead semi-random (the order depended on the
id() function.)  Now undo operations (as well as all
ResourceManagers) are sorted by creation order.
parent 3e771c9d
...@@ -679,6 +679,9 @@ class DB(object): ...@@ -679,6 +679,9 @@ class DB(object):
def versionEmpty(self, version): def versionEmpty(self, version):
return self._storage.versionEmpty(version) return self._storage.versionEmpty(version)
resource_counter_lock = threading.Lock()
resource_counter = 0
class ResourceManager(object): class ResourceManager(object):
"""Transaction participation for a version or undo resource.""" """Transaction participation for a version or undo resource."""
...@@ -689,8 +692,20 @@ class ResourceManager(object): ...@@ -689,8 +692,20 @@ class ResourceManager(object):
self.tpc_finish = self._db._storage.tpc_finish self.tpc_finish = self._db._storage.tpc_finish
self.tpc_abort = self._db._storage.tpc_abort self.tpc_abort = self._db._storage.tpc_abort
# Get a number from a simple thread-safe counter, then
# increment it, for the purpose of sorting ResourceManagers by
# creation order. This ensures that multiple ResourceManagers
# within a transaction commit in a predictable sequence.
resource_counter_lock.acquire()
try:
global resource_counter
self._count = resource_counter
resource_counter += 1
finally:
resource_counter_lock.release()
def sortKey(self): def sortKey(self):
return "%s:%s" % (self._db._storage.sortKey(), id(self)) return "%s:%016x" % (self._db._storage.sortKey(), self._count)
def tpc_begin(self, txn, sub=False): def tpc_begin(self, txn, sub=False):
if sub: if sub:
......
...@@ -728,6 +728,42 @@ class ZODBTests(unittest.TestCase): ...@@ -728,6 +728,42 @@ class ZODBTests(unittest.TestCase):
cn.close() cn.close()
cn2.close() cn2.close()
def checkMultipleUndoInOneTransaction(self):
# Verify that it's possible to perform multiple undo
# operations within a transaction. If ZODB performs the undo
# operations in a nondeterministic order, this test will often
# fail.
conn = self._db.open()
try:
root = conn.root()
# Add transactions that set root["state"] to (0..5)
for state_num in range(6):
transaction.begin()
root['state'] = state_num
transaction.get().note('root["state"] = %d' % state_num)
transaction.commit()
# Undo all but the first. Note that no work is actually
# performed yet.
transaction.begin()
log = self._db.undoLog()
for i in range(5):
self._db.undo(log[i]['id'])
transaction.get().note('undo states 1 through 5')
# Now attempt all those undo operations.
transaction.commit()
# Sanity check: we should be back to the first state.
self.assertEqual(root['state'], 0)
finally:
transaction.abort()
conn.close()
class PoisonedError(Exception): class PoisonedError(Exception):
pass pass
......
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