Commit ad23cfd3 authored by Barry Warsaw's avatar Barry Warsaw

Integration of application level conflict resolution, using the

ConflictResolution helper module.  Specifically,

class Full: add ConflictResolvingStorage to the base classes, so we
magically grow self.tryToResolveConflict().

store(): Keep a flag indicating whether we calculated the object's
pickle data via conflict resolution.  If so, we return the special
marker ResolvedSerial instead of the next available serial number.

Also, in the serial <> oserial clause, try to resolve the conflict
using ConflictResolvingStorage.tryToResolveConflict() and raise a
ConflictError only if that fails (i.e. returns a false value).
Otherwise, the resolution succeeded providing us with the data pickle
to use as the stored object's state.

transactionalUndo(): We need to keep an additional list of actions to
perform on a successful undo.  The first list keeps track of existing
revisions to point the new transaction at, but conflict resolution
provides us with a brand new pickle (or at least, a pickle for which
we've no idea what the lrevid pointer should be).  For those
situations we need to do a CommitLog.write_object() instead of a
CommitLog.write_object_undo() so as to get the new pickle into the
commit log.  Thus, newstates is a list keeping track of conflict
resolved object states, while (the existing) newrevs keeps track of
undo records where we already have the pickle in the database.

Also, in the clause where we raise an UndoError, first
tryToResolveConflict() and only if that fails do we raise the
UndoError.  Should it succeed, we append the record to newstates for
later.

Finally, in the clause were we're replaying the changes into the
commit log (because we now know that all undos will succeed), we first
replay the newrevs entries, then we replay the newstates entries,
making sure we return all the affected oids.

Note: these changes impose no regressions and pass all tests in
ConflictResolvingStorage and ConflictResolvingTransUndoStorage.
parent bea204e6
...@@ -4,7 +4,7 @@ See Minimal.py for an implementation of Berkeley storage that does not support ...@@ -4,7 +4,7 @@ See Minimal.py for an implementation of Berkeley storage that does not support
undo or versioning. undo or versioning.
""" """
__version__ = '$Revision: 1.26 $'[-2:][0] __version__ = '$Revision: 1.27 $'[-2:][0]
import struct import struct
import time import time
...@@ -17,6 +17,7 @@ from ZODB import POSException ...@@ -17,6 +17,7 @@ from ZODB import POSException
from ZODB import utils from ZODB import utils
from ZODB.referencesf import referencesf from ZODB.referencesf import referencesf
from ZODB.TimeStamp import TimeStamp from ZODB.TimeStamp import TimeStamp
from ZODB.ConflictResolution import ConflictResolvingStorage, ResolvedSerial
# BerkeleyBase.BerkeleyBase class provides some common functionality for both # BerkeleyBase.BerkeleyBase class provides some common functionality for both
# the Full and Minimal implementations. It in turn inherits from # the Full and Minimal implementations. It in turn inherits from
...@@ -40,7 +41,7 @@ DNE = '\377'*8 ...@@ -40,7 +41,7 @@ DNE = '\377'*8
class Full(BerkeleyBase): class Full(BerkeleyBase, ConflictResolvingStorage):
# #
# Overrides of base class methods # Overrides of base class methods
# #
...@@ -545,6 +546,7 @@ class Full(BerkeleyBase): ...@@ -545,6 +546,7 @@ class Full(BerkeleyBase):
if transaction is not self._transaction: if transaction is not self._transaction:
raise POSException.StorageTransactionError(self, transaction) raise POSException.StorageTransactionError(self, transaction)
conflictresolved = 0
self._lock_acquire() self._lock_acquire()
try: try:
# Check for conflict errors. JF says: under some circumstances, # Check for conflict errors. JF says: under some circumstances,
...@@ -561,10 +563,15 @@ class Full(BerkeleyBase): ...@@ -561,10 +563,15 @@ class Full(BerkeleyBase):
elif serial <> oserial: elif serial <> oserial:
# The object exists in the database, but the serial number # The object exists in the database, but the serial number
# given in the call is not the same as the last stored serial # given in the call is not the same as the last stored serial
# number. Raise a ConflictError. # number. First, attempt application level conflict
raise POSException.ConflictError( # resolution, and if that fails, raise a ConflictError.
'serial number mismatch (was: %s, has: %s)' % data = self.tryToResolveConflict(oid, oserial, serial, data)
(utils.U64(oserial), utils.U64(serial))) if data:
conflictresolved = 1
else:
raise POSException.ConflictError(
'serial number mismatch (was: %s, has: %s)' %
(utils.U64(oserial), utils.U64(serial)))
# Do we already know about this version? If not, we need to # Do we already know about this version? If not, we need to
# record the fact that a new version is being created. `version' # record the fact that a new version is being created. `version'
# will be the empty string when the transaction is storing on the # will be the empty string when the transaction is storing on the
...@@ -604,7 +611,10 @@ class Full(BerkeleyBase): ...@@ -604,7 +611,10 @@ class Full(BerkeleyBase):
self._commitlog.write_object(oid, vid, nvrevid, data, oserial) self._commitlog.write_object(oid, vid, nvrevid, data, oserial)
finally: finally:
self._lock_release() self._lock_release()
# Return our cached serial number for the object. # Return our cached serial number for the object. If conflict
# resolution occurred, we return the special marker value.
if conflictresolved:
return ResolvedSerial
return self._serial return self._serial
def transactionalUndo(self, tid, transaction): def transactionalUndo(self, tid, transaction):
...@@ -612,6 +622,7 @@ class Full(BerkeleyBase): ...@@ -612,6 +622,7 @@ class Full(BerkeleyBase):
raise POSException.StorageTransactionError(self, transaction) raise POSException.StorageTransactionError(self, transaction)
newrevs = [] newrevs = []
newstates = []
c = None c = None
self._lock_acquire() self._lock_acquire()
try: try:
...@@ -630,15 +641,8 @@ class Full(BerkeleyBase): ...@@ -630,15 +641,8 @@ class Full(BerkeleyBase):
# undoing either the current revision of the object, or we # undoing either the current revision of the object, or we
# must be restoring the exact same pickle (identity compared) # must be restoring the exact same pickle (identity compared)
# that would be restored if we were undoing the current # that would be restored if we were undoing the current
# revision. # revision. Otherwise, we attempt application level conflict
# # resolution. If that fails, we raise an exception.
# Note that we could do pickle equivalence comparisions
# instead. That would be "temporaly clean" in that we'd still
# be restoring the same state. We decided not to do this for
# now. Eventually, when we have application level conflict
# resolution, we can ask the object if it can resolve the
# state change, and then we'd reject the undo only if any of
# the state changes couldn't be resolved.
revid = self._serials[oid] revid = self._serials[oid]
if revid == tid: if revid == tid:
vid, nvrevid, lrevid, prevrevid = struct.unpack( vid, nvrevid, lrevid, prevrevid = struct.unpack(
...@@ -704,7 +708,22 @@ class Full(BerkeleyBase): ...@@ -704,7 +708,22 @@ class Full(BerkeleyBase):
elif target_lrevid == self._commitlog.get_prevrevid(oid): elif target_lrevid == self._commitlog.get_prevrevid(oid):
newrevs.append((oid, target_metadata)) newrevs.append((oid, target_metadata))
else: else:
raise POSException.UndoError, 'Cannot undo transaction' # Attempt application level conflict resolution
data = self.tryToResolveConflict(
oid, revid, tid, self._pickles[oid+target_lrevid])
if data:
# The problem is that the `data' pickle probably
# isn't in the _pickles table, and even if it
# were, we don't know what the lrevid pointer to
# it would be. This means we need to do a
# write_object() not a write_object_undo().
vid, nvrevid, lrevid, prevrevid = struct.unpack(
'>8s8s8s8s', target_metadata)
newstates.append((oid, vid, nvrevid, data,
prevrevid))
else:
raise POSException.UndoError(
'Cannot undo transaction')
# Okay, we've checked all the objects affected by the transaction # Okay, we've checked all the objects affected by the transaction
# we're about to undo, and everything looks good. So now we'll # we're about to undo, and everything looks good. So now we'll
# write to the log the new object records we intend to commit. # write to the log the new object records we intend to commit.
...@@ -720,6 +739,10 @@ class Full(BerkeleyBase): ...@@ -720,6 +739,10 @@ class Full(BerkeleyBase):
# will always overwrite previous ones, but it also means we'll # will always overwrite previous ones, but it also means we'll
# see duplicate oids in this iteration. # see duplicate oids in this iteration.
oids[oid] = 1 oids[oid] = 1
for oid, vid, nvrevid, data, prevrevid in newstates:
self._commitlog.write_object(oid, vid, nvrevid, data,
prevrevid)
oids[oid] = 1
return oids.keys() return oids.keys()
finally: finally:
if c: if c:
......
...@@ -4,7 +4,7 @@ See Minimal.py for an implementation of Berkeley storage that does not support ...@@ -4,7 +4,7 @@ See Minimal.py for an implementation of Berkeley storage that does not support
undo or versioning. undo or versioning.
""" """
__version__ = '$Revision: 1.26 $'[-2:][0] __version__ = '$Revision: 1.27 $'[-2:][0]
import struct import struct
import time import time
...@@ -17,6 +17,7 @@ from ZODB import POSException ...@@ -17,6 +17,7 @@ from ZODB import POSException
from ZODB import utils from ZODB import utils
from ZODB.referencesf import referencesf from ZODB.referencesf import referencesf
from ZODB.TimeStamp import TimeStamp from ZODB.TimeStamp import TimeStamp
from ZODB.ConflictResolution import ConflictResolvingStorage, ResolvedSerial
# BerkeleyBase.BerkeleyBase class provides some common functionality for both # BerkeleyBase.BerkeleyBase class provides some common functionality for both
# the Full and Minimal implementations. It in turn inherits from # the Full and Minimal implementations. It in turn inherits from
...@@ -40,7 +41,7 @@ DNE = '\377'*8 ...@@ -40,7 +41,7 @@ DNE = '\377'*8
class Full(BerkeleyBase): class Full(BerkeleyBase, ConflictResolvingStorage):
# #
# Overrides of base class methods # Overrides of base class methods
# #
...@@ -545,6 +546,7 @@ class Full(BerkeleyBase): ...@@ -545,6 +546,7 @@ class Full(BerkeleyBase):
if transaction is not self._transaction: if transaction is not self._transaction:
raise POSException.StorageTransactionError(self, transaction) raise POSException.StorageTransactionError(self, transaction)
conflictresolved = 0
self._lock_acquire() self._lock_acquire()
try: try:
# Check for conflict errors. JF says: under some circumstances, # Check for conflict errors. JF says: under some circumstances,
...@@ -561,10 +563,15 @@ class Full(BerkeleyBase): ...@@ -561,10 +563,15 @@ class Full(BerkeleyBase):
elif serial <> oserial: elif serial <> oserial:
# The object exists in the database, but the serial number # The object exists in the database, but the serial number
# given in the call is not the same as the last stored serial # given in the call is not the same as the last stored serial
# number. Raise a ConflictError. # number. First, attempt application level conflict
raise POSException.ConflictError( # resolution, and if that fails, raise a ConflictError.
'serial number mismatch (was: %s, has: %s)' % data = self.tryToResolveConflict(oid, oserial, serial, data)
(utils.U64(oserial), utils.U64(serial))) if data:
conflictresolved = 1
else:
raise POSException.ConflictError(
'serial number mismatch (was: %s, has: %s)' %
(utils.U64(oserial), utils.U64(serial)))
# Do we already know about this version? If not, we need to # Do we already know about this version? If not, we need to
# record the fact that a new version is being created. `version' # record the fact that a new version is being created. `version'
# will be the empty string when the transaction is storing on the # will be the empty string when the transaction is storing on the
...@@ -604,7 +611,10 @@ class Full(BerkeleyBase): ...@@ -604,7 +611,10 @@ class Full(BerkeleyBase):
self._commitlog.write_object(oid, vid, nvrevid, data, oserial) self._commitlog.write_object(oid, vid, nvrevid, data, oserial)
finally: finally:
self._lock_release() self._lock_release()
# Return our cached serial number for the object. # Return our cached serial number for the object. If conflict
# resolution occurred, we return the special marker value.
if conflictresolved:
return ResolvedSerial
return self._serial return self._serial
def transactionalUndo(self, tid, transaction): def transactionalUndo(self, tid, transaction):
...@@ -612,6 +622,7 @@ class Full(BerkeleyBase): ...@@ -612,6 +622,7 @@ class Full(BerkeleyBase):
raise POSException.StorageTransactionError(self, transaction) raise POSException.StorageTransactionError(self, transaction)
newrevs = [] newrevs = []
newstates = []
c = None c = None
self._lock_acquire() self._lock_acquire()
try: try:
...@@ -630,15 +641,8 @@ class Full(BerkeleyBase): ...@@ -630,15 +641,8 @@ class Full(BerkeleyBase):
# undoing either the current revision of the object, or we # undoing either the current revision of the object, or we
# must be restoring the exact same pickle (identity compared) # must be restoring the exact same pickle (identity compared)
# that would be restored if we were undoing the current # that would be restored if we were undoing the current
# revision. # revision. Otherwise, we attempt application level conflict
# # resolution. If that fails, we raise an exception.
# Note that we could do pickle equivalence comparisions
# instead. That would be "temporaly clean" in that we'd still
# be restoring the same state. We decided not to do this for
# now. Eventually, when we have application level conflict
# resolution, we can ask the object if it can resolve the
# state change, and then we'd reject the undo only if any of
# the state changes couldn't be resolved.
revid = self._serials[oid] revid = self._serials[oid]
if revid == tid: if revid == tid:
vid, nvrevid, lrevid, prevrevid = struct.unpack( vid, nvrevid, lrevid, prevrevid = struct.unpack(
...@@ -704,7 +708,22 @@ class Full(BerkeleyBase): ...@@ -704,7 +708,22 @@ class Full(BerkeleyBase):
elif target_lrevid == self._commitlog.get_prevrevid(oid): elif target_lrevid == self._commitlog.get_prevrevid(oid):
newrevs.append((oid, target_metadata)) newrevs.append((oid, target_metadata))
else: else:
raise POSException.UndoError, 'Cannot undo transaction' # Attempt application level conflict resolution
data = self.tryToResolveConflict(
oid, revid, tid, self._pickles[oid+target_lrevid])
if data:
# The problem is that the `data' pickle probably
# isn't in the _pickles table, and even if it
# were, we don't know what the lrevid pointer to
# it would be. This means we need to do a
# write_object() not a write_object_undo().
vid, nvrevid, lrevid, prevrevid = struct.unpack(
'>8s8s8s8s', target_metadata)
newstates.append((oid, vid, nvrevid, data,
prevrevid))
else:
raise POSException.UndoError(
'Cannot undo transaction')
# Okay, we've checked all the objects affected by the transaction # Okay, we've checked all the objects affected by the transaction
# we're about to undo, and everything looks good. So now we'll # we're about to undo, and everything looks good. So now we'll
# write to the log the new object records we intend to commit. # write to the log the new object records we intend to commit.
...@@ -720,6 +739,10 @@ class Full(BerkeleyBase): ...@@ -720,6 +739,10 @@ class Full(BerkeleyBase):
# will always overwrite previous ones, but it also means we'll # will always overwrite previous ones, but it also means we'll
# see duplicate oids in this iteration. # see duplicate oids in this iteration.
oids[oid] = 1 oids[oid] = 1
for oid, vid, nvrevid, data, prevrevid in newstates:
self._commitlog.write_object(oid, vid, nvrevid, data,
prevrevid)
oids[oid] = 1
return oids.keys() return oids.keys()
finally: finally:
if c: if c:
......
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