Commit 75bae1a6 authored by Jim Fulton's avatar Jim Fulton Committed by GitHub

Merge pull request #89 from zopefoundation/undo-refactor

Refactored FileStorage transactional undo
parents e080bdcc b563487e
...@@ -2,6 +2,12 @@ ...@@ -2,6 +2,12 @@
Change History Change History
================ ================
4.4.3 (unreleased)
==================
- Internal FileStorage-undo fixes that should allow undo in some cases
where it didn't work before.
4.4.2 (2016-07-08) 4.4.2 (2016-07-08)
================== ==================
......
...@@ -18,7 +18,8 @@ import six ...@@ -18,7 +18,8 @@ import six
import zope.interface import zope.interface
from ZODB.POSException import ConflictError from ZODB.POSException import ConflictError
from ZODB.loglevels import BLATHER from ZODB.loglevels import BLATHER
from ZODB._compat import BytesIO, PersistentUnpickler, PersistentPickler, _protocol from ZODB._compat import (
BytesIO, PersistentUnpickler, PersistentPickler, _protocol)
# Subtle: Python 2.x has pickle.PicklingError and cPickle.PicklingError, # Subtle: Python 2.x has pickle.PicklingError and cPickle.PicklingError,
# and these are unrelated classes! So we shouldn't use pickle.PicklingError, # and these are unrelated classes! So we shouldn't use pickle.PicklingError,
...@@ -73,7 +74,8 @@ def state(self, oid, serial, prfactory, p=''): ...@@ -73,7 +74,8 @@ def state(self, oid, serial, prfactory, p=''):
p = p or self.loadSerial(oid, serial) p = p or self.loadSerial(oid, serial)
p = self._crs_untransform_record_data(p) p = self._crs_untransform_record_data(p)
file = BytesIO(p) file = BytesIO(p)
unpickler = PersistentUnpickler(find_global, prfactory.persistent_load, file) unpickler = PersistentUnpickler(
find_global, prfactory.persistent_load, file)
unpickler.load() # skip the class tuple unpickler.load() # skip the class tuple
return unpickler.load() return unpickler.load()
...@@ -241,7 +243,8 @@ def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle, ...@@ -241,7 +243,8 @@ def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle,
prfactory = PersistentReferenceFactory() prfactory = PersistentReferenceFactory()
newpickle = self._crs_untransform_record_data(newpickle) newpickle = self._crs_untransform_record_data(newpickle)
file = BytesIO(newpickle) file = BytesIO(newpickle)
unpickler = PersistentUnpickler(find_global, prfactory.persistent_load, file) unpickler = PersistentUnpickler(
find_global, prfactory.persistent_load, file)
meta = unpickler.load() meta = unpickler.load()
if isinstance(meta, tuple): if isinstance(meta, tuple):
klass = meta[0] klass = meta[0]
......
...@@ -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,55 @@ class FileStorage( ...@@ -838,36 +840,55 @@ 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 if not current_data:
cdataptr == tipos current_data = self._loadBack_impl(oid, cdataptr)[0]
or
# Backpointers are different if data_to_be_undone != current_data:
self._loadBackPOS(oid, pos) != # OK, so the current data is different from
self._loadBackPOS(oid, cdataptr) # the data being undone. We can't just copy:
): copy = False
if pre and not tpos:
copy = 0 # we'll try to do conflict resolution if not pre:
else: # The transaction we're undoing has no
# We bail if: # previous state to merge with, so we
# - We don't have a previous record, which should # can't resolve a conflict.
# be impossible. raise UndoError(
raise UndoError("no previous record", oid) "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 +896,14 @@ class FileStorage( ...@@ -875,12 +896,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 +1025,8 @@ class FileStorage( ...@@ -1002,7 +1025,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 +1261,8 @@ class FileStorage( ...@@ -1237,7 +1261,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