Commit d717a685 authored by Jim Fulton's avatar Jim Fulton

Refactored FileStorage transactional undo

As part of a project to provide object-level commit locks for ZEO, I'm
refactiring FileStorage to maintain transaction-specific data in
Tranaction.data.  This involved undo.  In trying to figure this out, I
found:

- A bug in _undoDataInfo, which I verified with some tests and

- _transactionalUndoRecord was maddeningly difficult to reason about
  (and thus change).

I was concerned less by the bug than my inability to know whether a
change to the code would be correct.

So I refactored the code, mainly transactionalUndoRecord, to make the
code easier to understand, fixing some logic errors (I'm pretty sure)
along the way.  This included lots of comments. (Comments are much
easier to compose when you're working out logic you didn't
understand.)

In addition to makeing the code cleaner, it allows undo to be handled
in cases that weren't handled before.
parent a6c1713d
...@@ -787,13 +787,15 @@ class FileStorage( ...@@ -787,13 +787,15 @@ class FileStorage(
"""Return the tid, data pointer, and data for the oid record at pos """Return the tid, data pointer, and data for the oid record at pos
""" """
if tpos: if tpos:
pos = tpos - self._pos - self._thl itpos = tpos - self._pos - self._thl
pos = tpos
tpos = self._tfile.tell() tpos = self._tfile.tell()
h = self._tfmt._read_data_header(pos, oid) h = self._tfmt._read_data_header(itpos, oid)
afile = self._tfile afile = self._tfile
else: else:
h = self._read_data_header(pos, oid) h = self._read_data_header(pos, oid)
afile = self._file afile = self._file
if h.oid != oid: if h.oid != oid:
raise UndoError("Invalid undo transaction id", oid) raise UndoError("Invalid undo transaction id", oid)
...@@ -830,7 +832,7 @@ class FileStorage( ...@@ -830,7 +832,7 @@ class FileStorage(
pointer 0. pointer 0.
""" """
copy = 1 # Can we just copy a data pointer copy = True # Can we just copy a data pointer
# First check if it is possible to undo this record. # First check if it is possible to undo this record.
tpos = self._tindex.get(oid, 0) tpos = self._tindex.get(oid, 0)
...@@ -838,36 +840,60 @@ class FileStorage( ...@@ -838,36 +840,60 @@ class FileStorage(
tipos = tpos or ipos tipos = tpos or ipos
if tipos != pos: if tipos != pos:
# Eek, a later transaction modified the data, but, # The transaction being undone isn't current because:
# maybe it is pointing at the same data we are. # a) A later transaction was committed ipos != pos, or
ctid, cdataptr, cdata = self._undoDataInfo(oid, ipos, tpos) # b) A change was made in the current transaction. This
# could only be a previous undo in a multi-undo.
# (We don't allow multiple data managers with the same
# storage to participate in the same transaction.)
assert tipos > pos
# Get current data, as identified by tipos. We'll use
# it to decide if and how we can undo in this case.
ctid, cdataptr, current_data = self._undoDataInfo(oid, ipos, tpos)
if cdataptr != pos: if cdataptr != pos:
# We aren't sure if we are talking about the same data
# if cdataptr was == pos, then we'd be cool, because
# we're dealing with the same data.
# Because they aren't equal, we have to dig deeper
# Let's see if data to be undone and current data
# are the same. If not, we'll have to decide whether
# we should try conflict resolution.
try: try:
if ( data_to_be_undone = self._loadBack_impl(oid, pos)[0]
# The current record wrote a new pickle
cdataptr == tipos # Note that we use ``cdata or`` below, because
or # _loadBackPOS dosn't work with in-tranaaction
# Backpointers are different # data and because we don't need to bother if we
self._loadBackPOS(oid, pos) != # already have the data.
self._loadBackPOS(oid, cdataptr) if not current_data:
): current_data = self._loadBack_impl(oid, cdataptr)[0]
if pre and not tpos:
copy = 0 # we'll try to do conflict resolution if data_to_be_undone != current_data:
else: # OK, so the current data is different from
# We bail if: # the data being undone. We can't just copy:
# - We don't have a previous record, which should copy = False
# be impossible.
raise UndoError("no previous record", oid) if not pre:
# The transaction we're undoing has no
# previous state to merge with, so we
# can't resolve a conflict.
raise UndoError(
"Can't undo an add transaction followed by"
" conflicting transactions.", oid)
except KeyError: except KeyError:
# LoadBack gave us a key error. Bail. # LoadBack gave us a key error. Bail.
raise UndoError("_loadBack() failed", oid) raise UndoError("_loadBack() failed", oid)
# Return the data that should be written in the undo record. # Return the data that should be written in the undo record.
if not pre: if not pre:
# There is no previous revision, because the object creation # We're undoing object addition. We're doing this because
# is being undone. # subsequent transactions has no net effect on the state
# (possibly because some of them were undos).
return "", 0, ipos return "", 0, ipos
if copy: if copy:
...@@ -875,12 +901,14 @@ class FileStorage( ...@@ -875,12 +901,14 @@ class FileStorage(
return "", pre, ipos return "", pre, ipos
try: try:
bdata = self._loadBack_impl(oid, pre)[0] pre_data = self._loadBack_impl(oid, pre)[0]
except KeyError: except KeyError:
# couldn't find oid; what's the real explanation for this? # couldn't find oid; what's the real explanation for this?
raise UndoError("_loadBack() failed for %s", oid) raise UndoError("_loadBack() failed for %s", oid)
try: try:
data = self.tryToResolveConflict(oid, ctid, tid, bdata, cdata) data = self.tryToResolveConflict(
oid, ctid, tid, pre_data, current_data)
return data, 0, ipos return data, 0, ipos
except ConflictError: except ConflictError:
pass pass
...@@ -1002,7 +1030,8 @@ class FileStorage( ...@@ -1002,7 +1030,8 @@ class FileStorage(
# We're undoing a blob modification operation. # We're undoing a blob modification operation.
# We have to copy the blob data # We have to copy the blob data
tmp = mktemp(dir=self.fshelper.temp_dir) tmp = mktemp(dir=self.fshelper.temp_dir)
with self.openCommittedBlobFile(h.oid, userial) as sfp: with self.openCommittedBlobFile(
h.oid, userial) as sfp:
with open(tmp, 'wb') as dfp: with open(tmp, 'wb') as dfp:
cp(sfp, dfp) cp(sfp, dfp)
self._blob_storeblob(h.oid, self._tid, tmp) self._blob_storeblob(h.oid, self._tid, tmp)
...@@ -1237,7 +1266,8 @@ class FileStorage( ...@@ -1237,7 +1266,8 @@ class FileStorage(
continue continue
if len(line) != 16: if len(line) != 16:
raise ValueError("Bad record in ", self.blob_dir, '.removed') raise ValueError(
"Bad record in ", self.blob_dir, '.removed')
oid, tid = line[:8], line[8:] oid, tid = line[:8], line[8:]
path = fshelper.getBlobFilename(oid, tid) path = fshelper.getBlobFilename(oid, tid)
......
...@@ -801,3 +801,25 @@ class TransactionalUndoStorage: ...@@ -801,3 +801,25 @@ class TransactionalUndoStorage:
def checkIndicesInUndoLog(self): def checkIndicesInUndoLog(self):
self._exercise_info_indices("undoLog") self._exercise_info_indices("undoLog")
def checkUndoMultipleConflictResolution(self, reverse=False):
from .ConflictResolution import PCounter
db = DB(self._storage)
with db.transaction() as conn:
conn.root.x = PCounter()
for i in range(4):
with db.transaction() as conn:
conn.transaction_manager.get().note(str(i))
conn.root.x.inc()
ids = [l['id'] for l in db.undoLog(1, 3)]
if reverse:
ids = list(reversed(ids))
db.undoMultiple(ids)
transaction.commit()
def checkUndoMultipleConflictResolutionReversed(self):
self.checkUndoMultipleConflictResolution(True)
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