Learn more  » Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

edgify / persistent   python

Repository URL to install this package:

/ picklecache.py

##############################################################################
#
# Copyright (c) 2009 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
import gc

from weakref import WeakValueDictionary

from zope.interface import implementer
from zope.interface import classImplements

from persistent._compat import use_c_impl
from persistent._compat import PYPY
from persistent.interfaces import GHOST
from persistent.interfaces import IPickleCache
from persistent.interfaces import IExtendedPickleCache
from persistent.interfaces import OID_TYPE
from persistent.interfaces import UPTODATE
from persistent.persistence import PersistentPy
from persistent.persistence import _estimated_size_in_24_bits
from persistent.ring import Ring

__all__ = [
    'PickleCache',
    'PickleCachePy',
]

# We're tightly coupled to the PersistentPy implementation and access
# its internals.
# pylint:disable=protected-access



_OGA = object.__getattribute__
_OSA = object.__setattr__


def _sweeping_ring(f):
    # A decorator for functions in the PickleCache
    # that are sweeping the entire ring (mutating it);
    # serves as a pseudo-lock to not mutate the ring further
    # in other functions
    def locked(self, *args, **kwargs):
        self._is_sweeping_ring = True
        try:
            return f(self, *args, **kwargs)
        finally:
            self._is_sweeping_ring = False
    return locked


class _WeakValueDictionary(object):
    # Maps from OID -> Persistent object, but
    # only weakly references the Persistent object. This is similar
    # to ``weakref.WeakValueDictionary``, but is customized depending on the
    # platform. On PyPy, all objects can cheaply use a WeakRef, so that's
    # what we actually use. On CPython, though, ``PersistentPy`` cannot be weakly
    # referenced, so we rely on the fact that the ``id()`` of an object is its
    # memory location, and we use ``ctypes`` to cast that integer back to
    # the object.
    #
    # To remove stale addresses, we rely on the ``ffi.gc()`` object with the exact
    # same lifetime as the ``PersistentPy`` object. It calls us, we get the ``id``
    # back out of the CData, and clean up.
    if PYPY: # pragma: no cover
        def __init__(self):
            self._data = WeakValueDictionary()

        def _from_addr(self, addr):
            return addr

        def _save_addr(self, oid, obj):
            return obj

        cleanup_hook = None
    else:
        def __init__(self):
            # careful not to require ctypes at import time; most likely the
            # C implementation is in use.
            import ctypes

            self._data = {}
            self._addr_to_oid = {}
            self._cast = ctypes.cast
            self._py_object = ctypes.py_object

        def _save_addr(self, oid, obj):
            i = id(obj)
            self._addr_to_oid[i] = oid
            return i

        def _from_addr(self, addr):
            return self._cast(addr, self._py_object).value

        def cleanup_hook(self, cdata):
            oid = self._addr_to_oid.pop(cdata.pobj_id, None)
            self._data.pop(oid, None)

    def __contains__(self, oid):
        return oid in self._data

    def __len__(self):
        return len(self._data)

    def __setitem__(self, key, value):
        addr = self._save_addr(key, value)
        self._data[key] = addr

    def pop(self, oid):
        return self._from_addr(self._data.pop(oid))

    def items(self):
        from_addr = self._from_addr
        for oid, addr in self._data.items():
            yield oid, from_addr(addr)

    def get(self, oid, default=None):
        addr = self._data.get(oid, self)
        if addr is self:
            return default
        return self._from_addr(addr)

    def __getitem__(self, oid):
        addr = self._data[oid]
        return self._from_addr(addr)


@use_c_impl
# We actually implement IExtendedPickleCache, but
# the C version does not, and our interface declarations are
# copied over by the decorator. So we make the declaration
# of IExtendedPickleCache later.
@implementer(IPickleCache)
class PickleCache(object):

    # Tests may modify this to add additional types
    _CACHEABLE_TYPES = (type, PersistentPy)
    _SWEEPABLE_TYPES = (PersistentPy,)

    total_estimated_size = 0
    cache_size_bytes = 0

    # Set by functions that sweep the entire ring (via _sweeping_ring)
    # Serves as a pseudo-lock
    _is_sweeping_ring = False

    def __init__(self, jar, target_size=0, cache_size_bytes=0):
        # TODO: forward-port Dieter's bytes stuff
        self.jar = jar
        # We expect the jars to be able to have a pointer to
        # us; this is a reference cycle, but certain
        # aspects of invalidation and accessing depend on it.
        # The actual Connection objects we're used with do set this
        # automatically, but many test objects don't.
        # TODO: track this on the persistent objects themself?
        try:
            jar._cache = self
        except AttributeError:
            # Some ZODB tests pass in an object that cannot have an _cache
            pass
        self.cache_size = target_size
        self.drain_resistance = 0
        self.non_ghost_count = 0
        self.persistent_classes = {}
        self.data = _WeakValueDictionary()
        self.ring = Ring(self.data.cleanup_hook)
        self.cache_size_bytes = cache_size_bytes

    # IPickleCache API
    def __len__(self):
        """ See IPickleCache.
        """
        return (len(self.persistent_classes) +
                len(self.data))

    def __getitem__(self, oid):
        """ See IPickleCache.
        """
        value = self.data.get(oid, self)
        if value is not self:
            return value
        return self.persistent_classes[oid]

    def __setitem__(self, oid, value):
        """ See IPickleCache.
        """
        # The order of checks matters for C compatibility;
        # the ZODB tests depend on this

        # The C impl requires either a type or a Persistent subclass
        if not isinstance(value, self._CACHEABLE_TYPES):
            raise TypeError("Cache values must be persistent objects.")

        value_oid = value._p_oid
        if not isinstance(oid, OID_TYPE) or not isinstance(value_oid, OID_TYPE):
            raise TypeError('OID must be %s: key=%s _p_oid=%s' % (OID_TYPE, oid, value_oid))

        if value_oid != oid:
            raise ValueError("Cache key does not match oid")

        if oid in self.persistent_classes or oid in self.data:
            # 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
                # message.
                raise ValueError('A different object already has the same oid')
        # Match the C impl: it requires a jar. Let this raise AttributeError
        # if no jar is found.
        jar = value._p_jar
        if jar is None:
            raise ValueError("Cached object jar missing")
        # It also requires that it cannot be cached more than one place
        existing_cache = getattr(jar, '_cache', None) # type: PickleCache
        if (existing_cache is not None
                and existing_cache is not self
                and oid in existing_cache.data):
            raise ValueError("Cache values may only be in one cache.")

        if isinstance(value, type): # ZODB.persistentclass.PersistentMetaClass
            self.persistent_classes[oid] = value
        else:
            self.data[oid] = value
            if _OGA(value, '_p_state') != GHOST and value not in self.ring:
                self.ring.add(value)
                self.non_ghost_count += 1
            elif self.data.cleanup_hook:
                # Ensure we begin monitoring for ``value`` to
                # be deallocated.
                self.ring.ring_node_for(value)

    def __delitem__(self, oid):
        """ See IPickleCache.
        """
        if not isinstance(oid, OID_TYPE):
            raise TypeError('OID must be %s: %s' % (OID_TYPE, oid))
        if oid in self.persistent_classes:
            del self.persistent_classes[oid]
        else:
            pobj = self.data.pop(oid)
            self.ring.delete(pobj)

    def get(self, oid, default=None):
        """ See IPickleCache.
        """
        value = self.data.get(oid, self)
        if value is not self:
            return value
        return self.persistent_classes.get(oid, default)

    def mru(self, oid):
        """ 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 False # marker return for tests

        value = self.data[oid]

        was_in_ring = value in self.ring
        if not was_in_ring:
            if _OGA(value, '_p_state') != GHOST:
                self.ring.add(value)
                self.non_ghost_count += 1
        else:
            self.ring.move_to_head(value)
        return None

    def ringlen(self):
        """ See IPickleCache.
        """
        return len(self.ring)

    def items(self):
        """ See IPickleCache.
        """
        return self.data.items()

    def lru_items(self):
        """ See IPickleCache.
        """
        return [
            (obj._p_oid, obj)
            for obj in self.ring
        ]

    def klass_items(self):
        """ See IPickleCache.
        """
        return self.persistent_classes.items()

    def incrgc(self, ignored=None):
        """ See IPickleCache.
        """
        target = self.cache_size
        if self.drain_resistance >= 1:
            size = self.non_ghost_count
            target2 = size - 1 - (size // self.drain_resistance)
            if target2 < target:
                target = target2
        # return value for testing
        return self._sweep(target, self.cache_size_bytes)

    def full_sweep(self, target=None):
        """ See IPickleCache.
        """
        # return value for testing
        return self._sweep(0)

    minimize = full_sweep

    def new_ghost(self, oid, obj):
        """ See IPickleCache.
        """
        if obj._p_oid is not None:
            raise ValueError('Object already has oid')
        if obj._p_jar is not None:
            raise ValueError('Object already has jar')
        if oid in self.persistent_classes or oid in self.data:
            raise KeyError('Duplicate OID: %s' % oid)
        obj._p_oid = oid
        obj._p_jar = self.jar
        if not isinstance(obj, type):
            if obj._p_state != GHOST:
                # The C implementation sets this stuff directly,
                # but we delegate to the class. However, we must be
                # careful to avoid broken _p_invalidate and _p_deactivate
                # that don't call the super class. See ZODB's
                # testConnection.doctest_proper_ghost_initialization_with_empty__p_deactivate
                obj._p_invalidate_deactivate_helper(False)
        self[oid] = obj

    def reify(self, to_reify):
Loading ...