Commit 0cd1074f authored by Jim Fulton's avatar Jim Fulton

Bug Fixed:

  Conflict resolution failed when state included persistent references
  with classes that couldn't be imported.
parent 40bc7580
...@@ -600,31 +600,23 @@ class NastyConfict(Base, TestCase): ...@@ -600,31 +600,23 @@ class NastyConfict(Base, TestCase):
# to decref a NULL pointer if conflict resolution was fed 3 empty # to decref a NULL pointer if conflict resolution was fed 3 empty
# buckets. http://collector.zope.org/Zope/553 # buckets. http://collector.zope.org/Zope/553
def testThreeEmptyBucketsNoSegfault(self): def testThreeEmptyBucketsNoSegfault(self):
self.openDB() self.t[1] = 1
bucket = self.t._firstbucket
tm1 = transaction.TransactionManager() del self.t[1]
r1 = self.db.open(transaction_manager=tm1).root() state1 = bucket.__getstate__()
self.assertEqual(len(self.t), 0) state2 = bucket.__getstate__()
r1["t"] = b = self.t # an empty tree state3 = bucket.__getstate__()
tm1.commit() self.assert_(state2 is not state1 and
state2 is not state3 and
tm2 = transaction.TransactionManager() state3 is not state1)
r2 = self.db.open(transaction_manager=tm2).root() self.assert_(state2 == state1 and
copy = r2["t"] state3 == state1)
# Make sure all of copy is loaded. self.assertRaises(ConflictError, bucket._p_resolveConflict,
list(copy.values()) state1, state2, state3)
# When an empty BTree resolves conflicts, it computes the
# In one transaction, add and delete a key. # bucket state as None, so...
b[2] = 2 self.assertRaises(ConflictError, bucket._p_resolveConflict,
del b[2] None, None, None)
tm1.commit()
# In the other transaction, also add and delete a key.
b = copy
b[1] = 1
del b[1]
# If the commit() segfaults, the C code is still wrong for this case.
self.assertRaises(ConflictError, tm2.commit)
def testCantResolveBTreeConflict(self): def testCantResolveBTreeConflict(self):
# Test that a conflict involving two different changes to # Test that a conflict involving two different changes to
......
...@@ -2,6 +2,15 @@ ...@@ -2,6 +2,15 @@
Change History Change History
================ ================
3.10.4 (2011-11-16)
===================
Bugs Fixed
----------
- Conflict resolution failed when state included persistent references
with classes that couldn't be imported.
3.10.3 (2011-04-12) 3.10.3 (2011-04-12)
=================== ===================
......
...@@ -29,6 +29,14 @@ ResolvedSerial = 'rs' ...@@ -29,6 +29,14 @@ ResolvedSerial = 'rs'
class BadClassName(Exception): class BadClassName(Exception):
pass pass
class BadClass:
def __init__(self, *args):
self.args = args
def __reduce__(self):
raise BadClassName(*self.args)
_class_cache = {} _class_cache = {}
_class_cache_get = _class_cache.get _class_cache_get = _class_cache.get
def find_global(*args): def find_global(*args):
...@@ -48,6 +56,12 @@ def find_global(*args): ...@@ -48,6 +56,12 @@ def find_global(*args):
if cls == 1: if cls == 1:
# Not importable # Not importable
if (isinstance(args, tuple) and len(args) == 2 and
isinstance(args[0], basestring) and
isinstance(args[1], basestring)
):
return BadClass(*args)
else:
raise BadClassName(*args) raise BadClassName(*args)
return cls return cls
...@@ -110,6 +124,12 @@ class PersistentReference(object): ...@@ -110,6 +124,12 @@ class PersistentReference(object):
# see serialize.py, ObjectReader._persistent_load # see serialize.py, ObjectReader._persistent_load
if isinstance(data, tuple): if isinstance(data, tuple):
self.oid, self.klass = data self.oid, self.klass = data
if isinstance(self.klass, BadClass):
# We can't use the BadClass directly because, if
# resolution succeeds, there's no good way to pickle
# it. Fortunately, a class reference in a persistent
# reference is allowed to be a module+name tuple.
self.klass = self.klass.args
elif isinstance(data, str): elif isinstance(data, str):
self.oid = data self.oid = data
else: # a list else: # a list
...@@ -198,7 +218,6 @@ def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle, ...@@ -198,7 +218,6 @@ def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle,
if klass in _unresolvable: if klass in _unresolvable:
raise ConflictError raise ConflictError
newstate = unpickler.load()
inst = klass.__new__(klass, *newargs) inst = klass.__new__(klass, *newargs)
try: try:
...@@ -207,7 +226,20 @@ def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle, ...@@ -207,7 +226,20 @@ def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle,
_unresolvable[klass] = 1 _unresolvable[klass] = 1
raise ConflictError raise ConflictError
old = state(self, oid, oldSerial, prfactory)
oldData = self.loadSerial(oid, oldSerial)
if not committedData:
committedData = self.loadSerial(oid, committedSerial)
if newpickle == oldData:
# old -> new diff is empty, so merge is trivial
return committedData
if committedData == oldData:
# old -> committed diff is empty, so merge is trivial
return newpickle
newstate = unpickler.load()
old = state(self, oid, oldSerial, prfactory, oldData)
committed = state(self, oid, committedSerial, prfactory, committedData) committed = state(self, oid, committedSerial, prfactory, committedData)
resolved = resolve(old, committed, newstate) resolved = resolve(old, committed, newstate)
......
...@@ -13,27 +13,233 @@ ...@@ -13,27 +13,233 @@
############################################################################## ##############################################################################
import manuel.doctest import manuel.doctest
import manuel.footnote import manuel.footnote
import doctest
import manuel.capture import manuel.capture
import manuel.testing import manuel.testing
import persistent
import transaction
import unittest
import ZODB.ConflictResolution import ZODB.ConflictResolution
import ZODB.tests.util import ZODB.tests.util
import ZODB.POSException
import zope.testing.module import zope.testing.module
def setUp(test): def setUp(test):
ZODB.tests.util.setUp(test) ZODB.tests.util.setUp(test)
zope.testing.module.setUp(test, 'ConflictResolution_txt') zope.testing.module.setUp(test, 'ConflictResolution_txt')
ZODB.ConflictResolution._class_cache.clear()
ZODB.ConflictResolution._unresolvable.clear()
def tearDown(test): def tearDown(test):
zope.testing.module.tearDown(test) zope.testing.module.tearDown(test)
ZODB.tests.util.tearDown(test) ZODB.tests.util.tearDown(test)
ZODB.ConflictResolution._class_cache.clear() ZODB.ConflictResolution._class_cache.clear()
ZODB.ConflictResolution._unresolvable.clear()
class ResolveableWhenStateDoesNotChange(persistent.Persistent):
def _p_resolveConflict(old, committed, new):
raise ZODB.POSException.ConflictError
class Unresolvable(persistent.Persistent):
pass
def succeed_with_resolution_when_state_is_unchanged():
"""
If a conflicting change doesn't change the state, then don't even
bother calling _p_resolveConflict
>>> db = ZODB.DB('t.fs') # FileStorage!
>>> storage = db.storage
>>> conn = db.open()
>>> conn.root.x = ResolveableWhenStateDoesNotChange()
>>> conn.root.x.v = 1
>>> transaction.commit()
>>> serial1 = conn.root.x._p_serial
>>> conn.root.x.v = 2
>>> transaction.commit()
>>> serial2 = conn.root.x._p_serial
>>> oid = conn.root.x._p_oid
So, let's try resolving when the old and committed states are the same
bit the new state (pickle) is different:
>>> p = storage.tryToResolveConflict(
... oid, serial1, serial1, storage.loadSerial(oid, serial2))
>>> p == storage.loadSerial(oid, serial2)
True
And when the old and new states are the same bit the committed state
is different:
>>> p = storage.tryToResolveConflict(
... oid, serial2, serial1, storage.loadSerial(oid, serial1))
>>> p == storage.loadSerial(oid, serial2)
True
But we still conflict if both the committed and new are different than
the original:
>>> p = storage.tryToResolveConflict(
... oid, serial2, serial1, storage.loadSerial(oid, serial2))
... # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ConflictError: database conflict error (oid 0x01, ...
Of course, none of this applies if content doesn't support conflict resolution.
>>> conn.root.y = Unresolvable()
>>> conn.root.y.v = 1
>>> transaction.commit()
>>> oid = conn.root.y._p_oid
>>> serial = conn.root.y._p_serial
>>> p = storage.tryToResolveConflict(
... oid, serial, serial, storage.loadSerial(oid, serial))
... # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ConflictError: database conflict error (oid 0x02, ...
>>> db.close()
"""
class Resolveable(persistent.Persistent):
def _p_resolveConflict(self, old, committed, new):
resolved = {}
for k in old:
if k not in committed:
if k in new and new[k] == old[k]:
continue
raise ZODB.POSException.ConflictError
if k not in new:
if k in committed and committed[k] == old[k]:
continue
raise ZODB.POSException.ConflictError
if committed[k] != old[k]:
if new[k] == old[k]:
resolved[k] = committed[k]
continue
raise ZODB.POSException.ConflictError
if new[k] != old[k]:
if committed[k] == old[k]:
resolved[k] = new[k]
continue
raise ZODB.POSException.ConflictError
resolved[k] = old[k]
for k in new:
if k in old:
continue
if k in committed:
raise ZODB.POSException.ConflictError
resolved[k] = new[k]
for k in committed:
if k in old:
continue
if k in new:
raise ZODB.POSException.ConflictError
resolved[k] = committed[k]
return resolved
def resolve_even_when_referenced_classes_are_absent():
"""
We often want to be able to resolve even when there are pesistent
references to classes that can't be imported.
>>> db = ZODB.DB('t.fs') # FileStorage!
>>> storage = db.storage
>>> conn = db.open()
>>> conn.root.x = Resolveable()
>>> transaction.commit()
>>> oid = conn.root.x._p_oid
>>> serial = conn.root.x._p_serial
>>> class P(persistent.Persistent):
... pass
>>> conn.root.x.a = a = P()
>>> transaction.commit()
>>> serial1 = conn.root.x._p_serial
>>> del conn.root.x.a
>>> conn.root.x.b = b = P()
>>> transaction.commit()
>>> serial2 = conn.root.x._p_serial
Bwahaha:
>>> del P
Now, even though we can't import P, we can still resolve the conflict:
>>> p = storage.tryToResolveConflict(
... oid, serial1, serial, storage.loadSerial(oid, serial2))
>>> p = conn._reader.getState(p)
>>> sorted(p), p['a'] is a, p['b'] is b
(['a', 'b'], True, True)
Oooooof course, this won't work if the subobjects aren't persistent:
>>> class NP:
... pass
>>> conn.root.x = Resolveable()
>>> transaction.commit()
>>> oid = conn.root.x._p_oid
>>> serial = conn.root.x._p_serial
>>> conn.root.x.a = a = NP()
>>> transaction.commit()
>>> serial1 = conn.root.x._p_serial
>>> del conn.root.x.a
>>> conn.root.x.b = b = NP()
>>> transaction.commit()
>>> serial2 = conn.root.x._p_serial
Bwahaha:
>>> del NP
>>> storage.tryToResolveConflict(
... oid, serial1, serial, storage.loadSerial(oid, serial2))
... # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ConflictError: database conflict error (oid ...
>>> db.close()
"""
def test_suite(): def test_suite():
return manuel.testing.TestSuite( return unittest.TestSuite([
manuel.testing.TestSuite(
manuel.doctest.Manuel() manuel.doctest.Manuel()
+ manuel.footnote.Manuel() + manuel.footnote.Manuel()
+ manuel.capture.Manuel(), + manuel.capture.Manuel(),
'../ConflictResolution.txt', '../ConflictResolution.txt',
setUp=setUp, tearDown=tearDown, setUp=setUp, tearDown=tearDown,
) ),
doctest.DocTestSuite(
setUp=setUp, tearDown=tearDown),
])
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