[ZODB-Dev] BTree data loss bug under Python 3.x

Tres Seaver tseaver at palladion.com
Sat May 25 15:49:20 UTC 2013


-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 05/25/2013 02:50 AM, Marius Gedminas wrote:
> On Fri, May 24, 2013 at 09:31:55PM -0400, Tres Seaver wrote:
>> On 05/24/2013 01:28 PM, Marius Gedminas wrote:
>>> I was trying to debug a failing ZEO test case 
>>> (testConcurrentUpdates), and found out it had nothing to do with 
>>> ZEO.  There's a bug either in BTrees or in ZODB itself that
>>> causes data corruption under Python 3.x.
>>> 
>>> Here's a test case: 
>>> https://gist.github.com/mgedmin/5644876#file-zodbfail_simple-py
>> 
>> Thanks for the detective work!  I can confirm the bug, and that it
>> is in the BTrees C extensions (the tests pass after 'rm 
>> .tox/py32/lib/python3.2/site-packages/BTrees/*.so').
> 
> It's the COMPARE macro in _compat.h.  This patch fixes the data
> corruption:
> 
> diff --git a/BTrees/_compat.h b/BTrees/_compat.h index
> e004d54..4765df1 100644 --- a/BTrees/_compat.h +++ b/BTrees/_compat.h 
> @@ -28,8 +28,8 @@ #define TEXT_FORMAT PyUnicode_Format
> 
> #define COMPARE(lhs, rhs) \ -    PyObject_RichCompareBool((lhs),
> (rhs), Py_LT) > 0 ? -1 : \ -    (PyObject_RichCompareBool((lhs),
> (rhs), Py_EQ) > 0 ? 0 : 1) +    (PyObject_RichCompareBool((lhs),
> (rhs), Py_LT) > 0 ? -1 : \ +     (PyObject_RichCompareBool((lhs),
> (rhs), Py_EQ) > 0 ? 0 : 1))
> 
> 
> #else
> 
> 
> I'm now trying to write a failing unit test.  It's a bit difficult to 
> get access to C macros from Python tests ;-)
> 
> COMPARE is used in two places:
> 
> BTrees/objectkeymacros.h-#define TEST_KEY_SET_OR(V, KEY, TARGET) \ 
> BTrees/objectkeymacros.h:if ( ( (V) = COMPARE((KEY),(TARGET)) ),
> PyErr_Occurred() )
> 
> and
> 
> BTrees/objectvaluemacros.h:#define TEST_VALUE(VALUE, TARGET)
> COMPARE((VALUE),(TARGET))
> 
> We can ignore TEST_KEY_SET_OR, as it works fine there.
> 
> TEST_VALUE is used to implement BTree.byValue and bucket merging.
> The bug I was chasing down was caused by bucket mismerges, I think.
> 
> It should be easier to write a test for byValue.  In fact there is
> one, in test__base.py, but it's not run against real C-implemented
> BTrees.  I copied it over into test_OOBTree.py and discovered that it
> fails unexpectedly:
> 
> diff --git a/BTrees/tests/test_OOBTree.py
> b/BTrees/tests/test_OOBTree.py index bc046bc..e813f88 100644 ---
> a/BTrees/tests/test_OOBTree.py +++ b/BTrees/tests/test_OOBTree.py @@
> -99,9 +99,15 @@ class OOSetPyTest(ExtendedSetTests,
> unittest.TestCase):
> 
> class OOBTreeTest(BTreeTests, unittest.TestCase):
> 
> -    def _makeOne(self): +    def _makeOne(self, *args): from
> BTrees.OOBTree import OOBTree -        return OOBTree() +
> return OOBTree(*args) + +    def test_byValue(self): +        ITEMS =
> [(y, x) for x, y in enumerate('abcdefghijklmnopqrstuvwxyz')] +
> tree = self._makeOne(ITEMS) +
> self.assertEqual(list(tree.byValue(22)), +
> [(y, x) for x, y in ITEMS[22:]])
> 
> def testRejectDefaultComparison(self): # Check that passing int keys w
> default comparison fails. @@ -146,9 +152,9 @@ class
> OOBTreePyTest(OOBTreeTest): # default 'object' comparison semantics. 
> #class OOBTreePyTest(BTreeTests, unittest.TestCase):
> 
> -    def _makeOne(self): +    def _makeOne(self, *args): from
> BTrees.OOBTree import OOBTreePy -        return OOBTreePy() +
> return OOBTreePy(*args)
> 
> 
> 
> 
> The failure is
> 
> AssertionError: Lists differ: [(25, 'z'), (24, 'y'), (23, 'x... !=
> [(22, 'w'), (23, 'x'), (24, 'y...
> 
> First differing element 0: (25, 'z') (22, 'w')
> 
> - [(25, 'z'), (24, 'y'), (23, 'x'), (22, 'w')] + [(22, 'w'), (23,
> 'x'), (24, 'y'), (25, 'z')]
> 
> so the C-implemented BTrees return values in descending order, while
> the base Python implementation returns them in ascending order.  This
> happens on Python 2.7 as well, so it's not related to the COMPARE
> macro bug.
> 
> Does anybody know what the API of BTree.byValue() is supposed to be? 
> The interface just says "Return a sequence of (value, key) pairs,
> sorted by value", without clarifying if it means an ascending or a
> descending sort.

The docstring in the C module says:

    byValue(min) ->  list of value, key pairs

    Returns list of value, key pairs where the value is >= min.  The
    list is sorted by value.  Note that items() returns keys in the
    opposite order.

So I guess the descending order is intentional:  the call to '.reversed'
is certainly there.  Let's update the Python version to match, and use
your test to verify both.


Tres.
- -- 
===================================================================
Tres Seaver          +1 540-429-0999          tseaver at palladion.com
Palladion Software   "Excellence by Design"    http://palladion.com
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with undefined - http://www.enigmail.net/

iEYEARECAAYFAlGg3YAACgkQ+gerLs4ltQ5eeACgywzDPvD33nkM4Skd05PSOLMR
WPcAn03amWB8zcsmU+eQKnZBZ6AbYr/d
=7AlH
-----END PGP SIGNATURE-----



More information about the ZODB-Dev mailing list