Commit 829d155f authored by Jason Madden's avatar Jason Madden

Fix a race condition adding items to the pickle cache; explicitly support Jython too.

parent 80e0c6ee
...@@ -7,10 +7,12 @@ ...@@ -7,10 +7,12 @@
- The Python implementation of ``Persistent`` and ``PickleCache`` now - The Python implementation of ``Persistent`` and ``PickleCache`` now
behave more similarly to the C implementation. In particular, the behave more similarly to the C implementation. In particular, the
Python version can now run the complete ZODB unit test suite. Python version can now run the complete ZODB and ZEO test suites.
- Fix the hashcode of the Python ``TimeStamp`` on 32-bit platforms. - Fix the hashcode of the Python ``TimeStamp`` on 32-bit platforms.
- Add support for Jython 2.7.
4.0.9 (2015-04-08) 4.0.9 (2015-04-08)
------------------ ------------------
......
...@@ -46,6 +46,14 @@ _SWEEPABLE_TYPES = (Persistent,) ...@@ -46,6 +46,14 @@ _SWEEPABLE_TYPES = (Persistent,)
# Tests may modify this # Tests may modify this
_SWEEP_NEEDS_GC = not hasattr(sys, 'getrefcount') _SWEEP_NEEDS_GC = not hasattr(sys, 'getrefcount')
# On Jython, we need to explicitly ask it to monitor
# objects if we want a more deterministic GC
if hasattr(gc, 'monitorObject'): #pragma: no cover
_gc_monitor = gc.monitorObject
else:
def _gc_monitor(o):
pass
class RingNode(object): class RingNode(object):
# 32 byte fixed size wrapper. # 32 byte fixed size wrapper.
__slots__ = ('object', 'next', 'prev') __slots__ = ('object', 'next', 'prev')
...@@ -134,7 +142,10 @@ class PickleCache(object): ...@@ -134,7 +142,10 @@ class PickleCache(object):
# XXX # XXX
if oid in self.persistent_classes or oid in self.data: if oid in self.persistent_classes or oid in self.data:
if self.data[oid] is not value: # Have to be careful here, a GC might have just run
# and cleaned up the object
existing_data = self.get(oid)
if existing_data is not None and existing_data is not value:
# Raise the same type of exception as the C impl with the same # Raise the same type of exception as the C impl with the same
# message. # message.
raise ValueError('A different object already has the same oid') raise ValueError('A different object already has the same oid')
...@@ -153,6 +164,7 @@ class PickleCache(object): ...@@ -153,6 +164,7 @@ class PickleCache(object):
self.persistent_classes[oid] = value self.persistent_classes[oid] = value
else: else:
self.data[oid] = value self.data[oid] = value
_gc_monitor(value)
self.mru(oid) self.mru(oid)
def __delitem__(self, oid): def __delitem__(self, oid):
......
...@@ -18,6 +18,7 @@ import platform ...@@ -18,6 +18,7 @@ import platform
import sys import sys
py_impl = getattr(platform, 'python_implementation', lambda: None) py_impl = getattr(platform, 'python_implementation', lambda: None)
_is_pypy3 = py_impl() == 'PyPy' and sys.version_info[0] > 2 _is_pypy3 = py_impl() == 'PyPy' and sys.version_info[0] > 2
_is_jython = py_impl() == 'Jython'
#pylint: disable=R0904,W0212,E1101 #pylint: disable=R0904,W0212,E1101
...@@ -932,7 +933,7 @@ class _Persistent_Base(object): ...@@ -932,7 +933,7 @@ class _Persistent_Base(object):
self.assertEqual(inst.baz, 'bam') self.assertEqual(inst.baz, 'bam')
self.assertEqual(inst.qux, 'spam') self.assertEqual(inst.qux, 'spam')
if not _is_pypy3: if not _is_pypy3 and not _is_jython:
def test___setstate___interns_dict_keys(self): def test___setstate___interns_dict_keys(self):
class Derived(self._getTargetClass()): class Derived(self._getTargetClass()):
pass pass
......
...@@ -12,7 +12,8 @@ ...@@ -12,7 +12,8 @@
# #
############################################################################## ##############################################################################
import unittest import unittest
import gc
_is_jython = hasattr(gc, 'getJythonGCFlags')
_marker = object() _marker = object()
class PickleCacheTests(unittest.TestCase): class PickleCacheTests(unittest.TestCase):
...@@ -986,7 +987,22 @@ class PickleCacheTests(unittest.TestCase): ...@@ -986,7 +987,22 @@ class PickleCacheTests(unittest.TestCase):
# Nothing to test, just that it doesn't break # Nothing to test, just that it doesn't break
cache._invalidate(p._p_oid) cache._invalidate(p._p_oid)
def test_cache_garbage_collection_bytes_also_deactivates_object(self): if _is_jython:
def with_deterministic_gc(f):
def test(self):
old_flags = gc.getMonitorGlobal()
gc.setMonitorGlobal(True)
try:
f(self, force_collect=True)
finally:
gc.setMonitorGlobal(old_flags)
return test
else:
def with_deterministic_gc(f):
return f
@with_deterministic_gc
def test_cache_garbage_collection_bytes_also_deactivates_object(self, force_collect=False):
from persistent.interfaces import UPTODATE from persistent.interfaces import UPTODATE
from persistent._compat import _b from persistent._compat import _b
cache = self._makeOne() cache = self._makeOne()
...@@ -1028,6 +1044,8 @@ class PickleCacheTests(unittest.TestCase): ...@@ -1028,6 +1044,8 @@ class PickleCacheTests(unittest.TestCase):
# It also shrank the measured size of the cache; # It also shrank the measured size of the cache;
# this would fail under PyPy if _SWEEP_NEEDS_GC was False # this would fail under PyPy if _SWEEP_NEEDS_GC was False
if force_collect:
gc.collect()
self.assertEqual(len(cache), 1) self.assertEqual(len(cache), 1)
def test_invalidate_persistent_class_calls_p_invalidate(self): def test_invalidate_persistent_class_calls_p_invalidate(self):
......
...@@ -14,6 +14,11 @@ ...@@ -14,6 +14,11 @@
import operator import operator
import unittest import unittest
import platform
py_impl = getattr(platform, 'python_implementation', lambda: None)
_is_jython = py_impl() == 'Jython'
class Test__UTC(unittest.TestCase): class Test__UTC(unittest.TestCase):
def _getTargetClass(self): def _getTargetClass(self):
...@@ -271,26 +276,37 @@ class PyAndCComparisonTests(unittest.TestCase): ...@@ -271,26 +276,37 @@ class PyAndCComparisonTests(unittest.TestCase):
py = self._makePy(*self.now_ts_args) py = self._makePy(*self.now_ts_args)
self.assertEqual(hash(py), bit_32_hash) self.assertEqual(hash(py), bit_32_hash)
persistent.timestamp.c_long = ctypes.c_int64 persistent.timestamp.c_long = ctypes.c_int64
# call __hash__ directly to avoid interpreter truncation # call __hash__ directly to avoid interpreter truncation
# in hash() on 32-bit platforms # in hash() on 32-bit platforms
self.assertEqual(py.__hash__(), bit_64_hash) if not _is_jython:
self.assertEqual(py.__hash__(), bit_64_hash)
else:
# Jython 2.7's ctypes module doesn't properly
# implement the 'value' attribute by truncating.
# (It does for native calls, but not visibly to Python).
# Therefore we get back the full python long. The actual
# hash() calls are correct, though, because the JVM uses
# 32-bit ints for its hashCode methods.
self.assertEqual(py.__hash__(), 384009219096809580920179179233996861765753210540033L)
finally: finally:
persistent.timestamp.c_long = orig_c_long persistent.timestamp.c_long = orig_c_long
# These are *usually* aliases, but aren't required
# to be
if orig_c_long is ctypes.c_int32: if orig_c_long is ctypes.c_int32:
self.assertEqual(py.__hash__(), bit_32_hash) self.assertEqual(py.__hash__(), bit_32_hash)
elif orig_c_long is ctypes.c_int64: elif orig_c_long is ctypes.c_int64:
self.assertEqual(py.__hash__(), bit_64_hash) self.assertEqual(py.__hash__(), bit_64_hash)
else:
self.fail("Unknown bitness")
def test_hash_equal_constants(self): def test_hash_equal_constants(self):
# The simple constants make it easier to diagnose # The simple constants make it easier to diagnose
# a difference in algorithms # a difference in algorithms
import persistent.timestamp import persistent.timestamp
import ctypes import ctypes
is_32_bit = persistent.timestamp.c_long == ctypes.c_int32 # We get 32-bit hash values of 32-bit platforms, or on the JVM
is_32_bit = persistent.timestamp.c_long == ctypes.c_int32 or _is_jython
c, py = self._make_C_and_Py(b'\x00\x00\x00\x00\x00\x00\x00\x00') c, py = self._make_C_and_Py(b'\x00\x00\x00\x00\x00\x00\x00\x00')
self.assertEqual(hash(c), 8) self.assertEqual(hash(c), 8)
......
...@@ -87,6 +87,7 @@ setup(name='persistent', ...@@ -87,6 +87,7 @@ setup(name='persistent',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.4',
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Programming Language :: Python :: Implementation :: Jython",
"Framework :: ZODB", "Framework :: ZODB",
"Topic :: Database", "Topic :: Database",
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
......
[tox] [tox]
envlist = envlist =
# Jython support pending 2.7 support, due 2012-07-15 or so. See: # Jython 2.7rc2 does work, but unfortunately has an issue running
# http://fwierzbicki.blogspot.com/2012/03/adconion-to-fund-jython-27.html # with Tox 1.9.2 (http://bugs.jython.org/issue2325)
# py26,py27,py32,jython,pypy,coverage,docs # py26,py27,py27-pure,pypy,py32,py33,py34,pypy3,jython,coverage,docs
py26,py27,py27-pure,pypy,py32,py33,py34,pypy3,coverage,docs py26,py27,py27-pure,pypy,py32,py33,py34,pypy3,coverage,docs
[testenv] [testenv]
deps = deps =
zope.interface zope.interface
commands = commands =
python setup.py test -q python setup.py test -q
[testenv:jython] [testenv:jython]
commands = commands =
jython setup.py test -q jython setup.py test -q
[testenv:py27-pure] [testenv:py27-pure]
...@@ -22,13 +22,13 @@ setenv = ...@@ -22,13 +22,13 @@ setenv =
PURE_PYTHON = 1 PURE_PYTHON = 1
deps = deps =
{[testenv]deps} {[testenv]deps}
commands = commands =
python setup.py test -q python setup.py test -q
[testenv:coverage] [testenv:coverage]
basepython = basepython =
python2.6 python2.6
commands = commands =
nosetests --with-xunit --with-xcoverage nosetests --with-xunit --with-xcoverage
deps = deps =
zope.interface zope.interface
...@@ -39,7 +39,7 @@ deps = ...@@ -39,7 +39,7 @@ deps =
[testenv:docs] [testenv:docs]
basepython = basepython =
python2.6 python2.6
commands = commands =
sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest
deps = deps =
......
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