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):
# to decref a NULL pointer if conflict resolution was fed 3 empty
# buckets. http://collector.zope.org/Zope/553
def testThreeEmptyBucketsNoSegfault(self):
self.openDB()
tm1 = transaction.TransactionManager()
r1 = self.db.open(transaction_manager=tm1).root()
self.assertEqual(len(self.t), 0)
r1["t"] = b = self.t # an empty tree
tm1.commit()
tm2 = transaction.TransactionManager()
r2 = self.db.open(transaction_manager=tm2).root()
copy = r2["t"]
# Make sure all of copy is loaded.
list(copy.values())
# In one transaction, add and delete a key.
b[2] = 2
del b[2]
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)
self.t[1] = 1
bucket = self.t._firstbucket
del self.t[1]
state1 = bucket.__getstate__()
state2 = bucket.__getstate__()
state3 = bucket.__getstate__()
self.assert_(state2 is not state1 and
state2 is not state3 and
state3 is not state1)
self.assert_(state2 == state1 and
state3 == state1)
self.assertRaises(ConflictError, bucket._p_resolveConflict,
state1, state2, state3)
# When an empty BTree resolves conflicts, it computes the
# bucket state as None, so...
self.assertRaises(ConflictError, bucket._p_resolveConflict,
None, None, None)
def testCantResolveBTreeConflict(self):
# Test that a conflict involving two different changes to
......
......@@ -2,6 +2,15 @@
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)
===================
......
......@@ -29,6 +29,14 @@ ResolvedSerial = 'rs'
class BadClassName(Exception):
pass
class BadClass:
def __init__(self, *args):
self.args = args
def __reduce__(self):
raise BadClassName(*self.args)
_class_cache = {}
_class_cache_get = _class_cache.get
def find_global(*args):
......@@ -48,7 +56,13 @@ def find_global(*args):
if cls == 1:
# Not importable
raise BadClassName(*args)
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)
return cls
def state(self, oid, serial, prfactory, p=''):
......@@ -110,6 +124,12 @@ class PersistentReference(object):
# see serialize.py, ObjectReader._persistent_load
if isinstance(data, tuple):
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):
self.oid = data
else: # a list
......@@ -198,7 +218,6 @@ def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle,
if klass in _unresolvable:
raise ConflictError
newstate = unpickler.load()
inst = klass.__new__(klass, *newargs)
try:
......@@ -207,7 +226,20 @@ def tryToResolveConflict(self, oid, committedSerial, oldSerial, newpickle,
_unresolvable[klass] = 1
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)
resolved = resolve(old, committed, newstate)
......
......@@ -13,27 +13,233 @@
##############################################################################
import manuel.doctest
import manuel.footnote
import doctest
import manuel.capture
import manuel.testing
import persistent
import transaction
import unittest
import ZODB.ConflictResolution
import ZODB.tests.util
import ZODB.POSException
import zope.testing.module
def setUp(test):
ZODB.tests.util.setUp(test)
zope.testing.module.setUp(test, 'ConflictResolution_txt')
ZODB.ConflictResolution._class_cache.clear()
ZODB.ConflictResolution._unresolvable.clear()
def tearDown(test):
zope.testing.module.tearDown(test)
ZODB.tests.util.tearDown(test)
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():
return manuel.testing.TestSuite(
manuel.doctest.Manuel()
+ manuel.footnote.Manuel()
+ manuel.capture.Manuel(),
'../ConflictResolution.txt',
setUp=setUp, tearDown=tearDown,
)
return unittest.TestSuite([
manuel.testing.TestSuite(
manuel.doctest.Manuel()
+ manuel.footnote.Manuel()
+ manuel.capture.Manuel(),
'../ConflictResolution.txt',
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