[Zodb-checkins] SVN: ZODB/trunk/src/ Bug Fixed:

Jim Fulton jim at zope.com
Sun Nov 20 11:44:14 UTC 2011


Log message for revision 123443:
  Bug Fixed:
    Conflict resolution failed when state included persistent references
    (local or cross-database) with classes that couldn't be imported.
  

Changed:
  U   ZODB/trunk/src/BTrees/tests/testConflict.py
  U   ZODB/trunk/src/CHANGES.txt
  U   ZODB/trunk/src/ZODB/ConflictResolution.py
  U   ZODB/trunk/src/ZODB/tests/testconflictresolution.py

-=-
Modified: ZODB/trunk/src/BTrees/tests/testConflict.py
===================================================================
--- ZODB/trunk/src/BTrees/tests/testConflict.py	2011-11-20 11:37:18 UTC (rev 123442)
+++ ZODB/trunk/src/BTrees/tests/testConflict.py	2011-11-20 11:44:13 UTC (rev 123443)
@@ -600,32 +600,24 @@
     # to decref a NULL pointer if conflict resolution was fed 3 empty
     # buckets.  http://collector.zope.org/Zope/553
     def testThreeEmptyBucketsNoSegfault(self):
-        self.openDB()
+        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)
 
-        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)
-
     def testCantResolveBTreeConflict(self):
         # Test that a conflict involving two different changes to
         # an internal BTree node is unresolvable.  An internal node

Modified: ZODB/trunk/src/CHANGES.txt
===================================================================
--- ZODB/trunk/src/CHANGES.txt	2011-11-20 11:37:18 UTC (rev 123442)
+++ ZODB/trunk/src/CHANGES.txt	2011-11-20 11:44:13 UTC (rev 123443)
@@ -13,15 +13,30 @@
   ZODB.event.notify to provide your own event handling, although
   zope.event is recommended.
 
-Bugs Fixed
-----------
-
 - BTrees allowed object keys with insane comparison. (Comparison
   inherited from object, which compares based on in-process address.)
   Now BTrees raise TypeError if an attempt is made to save a key with
   comparison inherited from object. (This doesn't apply to old-style
   class instances.)
 
+3.10.5 (2011-11-19)
+===================
+
+Bugs Fixed
+----------
+
+- Conflict resolution failed when state included cross-database
+  persistent references with classes that couldn't be imported.
+
+3.10.4 (2011-11-17)
+===================
+
+Bugs Fixed
+----------
+
+- Conflict resolution failed when state included persistent references
+  with classes that couldn't be imported.
+
 3.10.3 (2011-04-12)
 ===================
 

Modified: ZODB/trunk/src/ZODB/ConflictResolution.py
===================================================================
--- ZODB/trunk/src/ZODB/ConflictResolution.py	2011-11-20 11:37:18 UTC (rev 123442)
+++ ZODB/trunk/src/ZODB/ConflictResolution.py	2011-11-20 11:44:13 UTC (rev 123443)
@@ -29,6 +29,14 @@
 class BadClassName(Exception):
     pass
 
+class BadClass(object):
+
+    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 @@
 
     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=''):
@@ -109,7 +123,13 @@
         self.data = data
         # see serialize.py, ObjectReader._persistent_load
         if isinstance(data, tuple):
-            self.oid, self.klass = data
+            self.oid, klass = data
+            if isinstance(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.data = self.oid, klass.args
         elif isinstance(data, str):
             self.oid = data
         else: # a list
@@ -120,7 +140,10 @@
             #    or persistent weakref: (oid, database_name)
             # else it is a weakref: reference_type
             if reference_type == 'm':
-                self.database_name, self.oid, self.klass = data[1]
+                self.database_name, self.oid, klass = data[1]
+                if isinstance(klass, BadClass):
+                    # see above wrt BadClass
+                    data[1] = self.database_name, self.oid, klass.args
             elif reference_type == 'n':
                 self.database_name, self.oid = data[1]
             elif reference_type == 'w':
@@ -153,6 +176,16 @@
     def __getstate__(self):
         raise PicklingError("Can't pickle PersistentReference")
 
+
+    @property
+    def klass(self):
+        # for tests
+        data = self.data
+        if isinstance(data, tuple):
+            return data[1]
+        elif isinstance(data, list) and data[0] == 'm':
+            return data[1][2]
+
 class PersistentReferenceFactory:
 
     data = None
@@ -198,7 +231,6 @@
         if klass in _unresolvable:
             raise ConflictError
 
-        newstate = unpickler.load()
         inst = klass.__new__(klass, *newargs)
 
         try:
@@ -207,7 +239,20 @@
             _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)

Modified: ZODB/trunk/src/ZODB/tests/testconflictresolution.py
===================================================================
--- ZODB/trunk/src/ZODB/tests/testconflictresolution.py	2011-11-20 11:37:18 UTC (rev 123442)
+++ ZODB/trunk/src/ZODB/tests/testconflictresolution.py	2011-11-20 11:44:13 UTC (rev 123443)
@@ -13,27 +13,296 @@
 ##############################################################################
 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.
+
+    >>> class P(persistent.Persistent):
+    ...     pass
+
+    >>> 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
+
+    >>> conn.root.x.a = P()
+    >>> transaction.commit()
+    >>> aid = conn.root.x.a._p_oid
+    >>> serial1 = conn.root.x._p_serial
+
+    >>> del conn.root.x.a
+    >>> conn.root.x.b = P()
+    >>> transaction.commit()
+    >>> serial2 = conn.root.x._p_serial
+
+Bwahaha:
+
+    >>> P_aside = P
+    >>> 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))
+
+And load the pickle:
+
+    >>> conn2 = db.open()
+    >>> P = P_aside
+    >>> p = conn2._reader.getState(p)
+    >>> sorted(p), p['a'] is conn2.get(aid), p['b'] is conn2.root.x.b
+    (['a', 'b'], True, True)
+
+    >>> isinstance(p['a'], P) and isinstance(p['b'], P)
+    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 resolve_even_when_xdb_referenced_classes_are_absent():
+    """Cross-database persistent refs!
+
+    >>> class P(persistent.Persistent):
+    ...     pass
+
+    >>> databases = {}
+    >>> db = ZODB.DB('t.fs', databases=databases, database_name='')
+    >>> db2 = ZODB.DB('o.fs', databases=databases, database_name='o')
+    >>> storage = db.storage
+    >>> conn = db.open()
+    >>> conn.root.x = Resolveable()
+    >>> transaction.commit()
+    >>> oid = conn.root.x._p_oid
+    >>> serial = conn.root.x._p_serial
+
+    >>> p = P(); conn.get_connection('o').add(p)
+    >>> conn.root.x.a = p
+    >>> transaction.commit()
+    >>> aid = conn.root.x.a._p_oid
+    >>> serial1 = conn.root.x._p_serial
+
+    >>> del conn.root.x.a
+    >>> p = P(); conn.get_connection('o').add(p)
+    >>> conn.root.x.b = p
+    >>> transaction.commit()
+    >>> serial2 = conn.root.x._p_serial
+
+    >>> del p
+
+Bwahaha:
+
+    >>> P_aside = P
+    >>> 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))
+
+And load the pickle:
+
+    >>> conn2 = db.open()
+    >>> conn2o = conn2.get_connection('o')
+    >>> P = P_aside
+    >>> p = conn2._reader.getState(p)
+    >>> sorted(p), p['a'] is conn2o.get(aid), p['b'] is conn2.root.x.b
+    (['a', 'b'], True, True)
+
+    >>> isinstance(p['a'], P) and isinstance(p['b'], P)
+    True
+
+    >>> db.close()
+    >>> db2.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),
+        ])
 



More information about the Zodb-checkins mailing list