Commit 9196be75 authored by Jason Madden's avatar Jason Madden

More on C and ZODB compatibility for Python PickleCache and Persistent. There...

More on C and ZODB compatibility for Python PickleCache and Persistent. There are some interconnected lifetime issues that this solves for ZODB's cache tests, and what appear to be some genuine bugs in invalidation.
parent f16959ff
...@@ -14,3 +14,4 @@ coverage.xml ...@@ -14,3 +14,4 @@ coverage.xml
*.egg-info *.egg-info
.installed.cfg .installed.cfg
.dir-locals.el .dir-locals.el
dist
...@@ -350,15 +350,26 @@ class Persistent(object): ...@@ -350,15 +350,26 @@ class Persistent(object):
_OSA(self, '_Persistent__flags', before) _OSA(self, '_Persistent__flags', before)
raise raise
# In the C implementation, _p_invalidate winds up calling
# _p_deactivate. There are ZODB tests that depend on this;
# it's not documented but there may be code in the wild
# that does as well
def _p_deactivate(self): def _p_deactivate(self):
""" See IPersistent. """ See IPersistent.
""" """
if self.__flags is not None and not self.__flags: if self.__flags is not None and not self.__flags:
self._p_invalidate() self._p_invalidate_deactivate_helper()
def _p_invalidate(self): def _p_invalidate(self):
""" See IPersistent. """ See IPersistent.
""" """
# If we think we have changes, we must pretend
# like we don't so that deactivate does its job
_OSA(self, '_Persistent__flags', 0)
self._p_deactivate()
def _p_invalidate_deactivate_helper(self):
if self.__jar is not None: if self.__jar is not None:
if self.__flags is not None: if self.__flags is not None:
_OSA(self, '_Persistent__flags', None) _OSA(self, '_Persistent__flags', None)
...@@ -374,6 +385,8 @@ class Persistent(object): ...@@ -374,6 +385,8 @@ class Persistent(object):
if cache is not None: if cache is not None:
cache.update_object_size_estimation(self.__oid, cache.update_object_size_estimation(self.__oid,
-1) -1)
# See notes in PickleCache.sweep for why we have to do this
cache._persistent_deactivate_ran = True
def _p_getattr(self, name): def _p_getattr(self, name):
""" See IPersistent. """ See IPersistent.
......
...@@ -22,6 +22,7 @@ from persistent.interfaces import GHOST ...@@ -22,6 +22,7 @@ from persistent.interfaces import GHOST
from persistent.interfaces import IPickleCache from persistent.interfaces import IPickleCache
from persistent.interfaces import STICKY from persistent.interfaces import STICKY
from persistent.interfaces import OID_TYPE from persistent.interfaces import OID_TYPE
from persistent.interfaces import UPTODATE
from persistent import Persistent from persistent import Persistent
# Tests may modify this to add additional types # Tests may modify this to add additional types
...@@ -35,12 +36,23 @@ class RingNode(object): ...@@ -35,12 +36,23 @@ class RingNode(object):
self.next = next self.next = next
self.prev = prev self.prev = prev
def _sweeping_ring(f):
def locked(self, *args, **kwargs):
self._is_sweeping_ring = True
try:
f(self, *args, **kwargs)
finally:
self._is_sweeping_ring = False
return locked
@implementer(IPickleCache) @implementer(IPickleCache)
class PickleCache(object): class PickleCache(object):
total_estimated_size = 0 total_estimated_size = 0
cache_size_bytes = 0 cache_size_bytes = 0
_is_sweeping_ring = False
def __init__(self, jar, target_size=0, cache_size_bytes=0): def __init__(self, jar, target_size=0, cache_size_bytes=0):
# TODO: forward-port Dieter's bytes stuff # TODO: forward-port Dieter's bytes stuff
self.jar = jar self.jar = jar
...@@ -111,11 +123,7 @@ class PickleCache(object): ...@@ -111,11 +123,7 @@ class PickleCache(object):
self.persistent_classes[oid] = value self.persistent_classes[oid] = value
else: else:
self.data[oid] = value self.data[oid] = value
if value._p_state != GHOST: self.mru(oid)
self.non_ghost_count += 1
mru = self.ring.prev
self.ring.prev = node = RingNode(value, self.ring, mru)
mru.next = node
def __delitem__(self, oid): def __delitem__(self, oid):
""" See IPickleCache. """ See IPickleCache.
...@@ -137,6 +145,7 @@ class PickleCache(object): ...@@ -137,6 +145,7 @@ class PickleCache(object):
def get(self, oid, default=None): def get(self, oid, default=None):
""" See IPickleCache. """ See IPickleCache.
""" """
value = self.data.get(oid, self) value = self.data.get(oid, self)
if value is not self: if value is not self:
return value return value
...@@ -145,6 +154,11 @@ class PickleCache(object): ...@@ -145,6 +154,11 @@ class PickleCache(object):
def mru(self, oid): def mru(self, oid):
""" See IPickleCache. """ See IPickleCache.
""" """
if self._is_sweeping_ring:
# accessess during sweeping, such as with an
# overridden _p_deactivate, don't mutate the ring
# because that could leave it inconsistent
return
node = self.ring.next node = self.ring.next
while node is not self.ring and node.object._p_oid != oid: while node is not self.ring and node.object._p_oid != oid:
node = node.next node = node.next
...@@ -156,6 +170,7 @@ class PickleCache(object): ...@@ -156,6 +170,7 @@ class PickleCache(object):
self.ring.prev = node = RingNode(value, self.ring, mru) self.ring.prev = node = RingNode(value, self.ring, mru)
mru.next = node mru.next = node
else: else:
assert node.object._p_oid == oid
# remove from old location # remove from old location
node.prev.next, node.next.prev = node.next, node.prev node.prev.next, node.next.prev = node.next, node.prev
# splice into new # splice into new
...@@ -195,10 +210,10 @@ class PickleCache(object): ...@@ -195,10 +210,10 @@ class PickleCache(object):
def incrgc(self, ignored=None): def incrgc(self, ignored=None):
""" See IPickleCache. """ See IPickleCache.
""" """
target = self.target_size target = self.cache_size
if self.drain_resistance >= 1: if self.drain_resistance >= 1:
size = self.non_ghost_count size = self.non_ghost_count
target2 = size - 1 - (size / self.drain_resistance) target2 = size - 1 - (size // self.drain_resistance)
if target2 < target: if target2 < target:
target = target2 target = target2
self._sweep(target, self.cache_size_bytes) self._sweep(target, self.cache_size_bytes)
...@@ -287,16 +302,34 @@ class PickleCache(object): ...@@ -287,16 +302,34 @@ class PickleCache(object):
cache_klass_count = property(lambda self: len(self.persistent_classes)) cache_klass_count = property(lambda self: len(self.persistent_classes))
# Helpers # Helpers
# Set to true when a deactivation happens in our code. For
# compatibility with the C implementation, we can only remove the
# node and decrement our non-ghost count if our implementation
# actually runs (broken subclasses can forget to call super; ZODB
# has tests for this). This gets set to false everytime we examine
# a node and checked afterwards. The C implementation has a very
# incestuous relatiounship between cPickleCache and cPersistence:
# the pickle cache calls _p_deactivate, which is responsible for
# both decrementing the non-ghost count and removing its node from
# the cache ring. We're trying to keep that to a minimum, but
# there's no way around it if we want full compatibility
_persistent_deactivate_ran = False
@_sweeping_ring
def _sweep(self, target, target_size_bytes=0): def _sweep(self, target, target_size_bytes=0):
# lock # lock
node = self.ring.next node = self.ring.next
ejected = 0 ejected = 0
while (node is not self.ring while (node is not self.ring
and (self.non_ghost_count > target and ( self.non_ghost_count > target
or (target_size_bytes and self.total_estimated_size > target_size_bytes))): or (target_size_bytes and self.total_estimated_size > target_size_bytes))):
if node.object._p_state not in (STICKY, CHANGED): if node.object._p_state == UPTODATE:
ejected += 1 # The C implementation will only evict things that are specifically
node.prev.next, node.next.prev = node.next, node.prev # in the up-to-date state
self._persistent_deactivate_ran = False
# sweeping an object out of the cache should also # sweeping an object out of the cache should also
# ghost it---that's what C does. This winds up # ghost it---that's what C does. This winds up
# calling `update_object_size_estimation`. # calling `update_object_size_estimation`.
...@@ -304,10 +337,18 @@ class PickleCache(object): ...@@ -304,10 +337,18 @@ class PickleCache(object):
# it removes itself from the `data` dictionary. # it removes itself from the `data` dictionary.
# If we're under PyPy or Jython, we need to run a GC collection # If we're under PyPy or Jython, we need to run a GC collection
# to make this happen...this is only noticeable though, when # to make this happen...this is only noticeable though, when
# we eject objects # we eject objects. Also, note that we can only take any of these
# actions if our _p_deactivate ran, in case of buggy subclasses.
# see _persistent_deactivate_ran
node.object._p_deactivate() node.object._p_deactivate()
node.object = None if (self._persistent_deactivate_ran
self.non_ghost_count -= 1 # Test-cases sneak in non-Persistent objects, sigh, so naturally
# they don't cooperate (without this check a bunch of test_picklecache
#breaks)
or not isinstance(node.object, Persistent)):
ejected += 1
self.__remove_from_ring(node)
node = node.next node = node.next
if ejected: if ejected:
# TODO: Only do this on PyPy/Jython? # TODO: Only do this on PyPy/Jython?
...@@ -316,21 +357,26 @@ class PickleCache(object): ...@@ -316,21 +357,26 @@ class PickleCache(object):
gc.collect() gc.collect()
return ejected return ejected
@_sweeping_ring
def _invalidate(self, oid): def _invalidate(self, oid):
value = self.data.get(oid) value = self.data.get(oid)
if value is not None and value._p_state != GHOST: if value is not None and value._p_state != GHOST:
value._p_invalidate() value._p_invalidate()
node = self.ring.next node = self.ring.next
while True: while node is not self.ring:
if node is self.ring:
break # pragma: no cover belt-and-suspenders
if node.object is value: if node.object is value:
node.prev.next, node.next.prev = node.next, node.prev self.__remove_from_ring(node)
break break
node = node.next node = node.next
elif oid in self.persistent_classes: elif oid in self.persistent_classes:
del self.persistent_classes[oid] del self.persistent_classes[oid]
def __remove_from_ring(self, node):
"Take the node, which previously contained a non-ghost, out of the ring"
node.object = None
node.prev.next, node.next.prev = node.next, node.prev
self.non_ghost_count -= 1
def _estimated_size_in_24_bits(value): def _estimated_size_in_24_bits(value):
if value > 1073741696: if value > 1073741696:
return 16777215 return 16777215
......
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