Source code for Bcfg2.Server.Cache

""" ``Bcfg2.Server.Cache`` is an implementation of a simple
memory-backed cache. Right now this doesn't provide many features, but
more (time-based expiration, etc.)  can be added as necessary.

The normal workflow is to get a Cache object, which is simply a dict
interface to the unified cache that automatically uses a certain tag
set.  For instance:

.. code-block:: python

    groupcache = Bcfg2.Server.Cache.Cache("Probes", "probegroups")
    groupcache['foo.example.com'] = ['group1', 'group2']

This would create a Cache object that automatically tags its entries
with ``frozenset(["Probes", "probegroups"])``, and store the list
``['group1', 'group1']`` with the *additional* tag
``foo.example.com``.  So the unified backend cache would then contain
a single entry:

.. code-block:: python

    {frozenset(["Probes", "probegroups", "foo.example.com"]):
     ['group1', 'group2']}

In addition to the dict interface, Cache objects (returned from
:func:`Bcfg2.Server.Cache.Cache`) have one additional method,
``expire()``, which is mostly identical to
:func:`Bcfg2.Server.Cache.expire`, except that it is specific to the
tag set of the cache object.  E.g., to expire all ``foo.example.com``
records for a given cache, you could do:

.. code-block:: python

    groupcache = Bcfg2.Server.Cache.Cache("Probes", "probegroups")
    groupcache.expire("foo.example.com")

This is mostly functionally identical to:

.. code-block:: python

    Bcfg2.Server.Cache.expire("Probes", "probegroups", "foo.example.com")

It's not completely identical, though; the first example will expire,
at most, exactly one item from the cache.  The second example will
expire all items that are tagged with a superset of the given tags.
To illustrate the difference, consider the following two examples:

.. code-block:: python

    groupcache = Bcfg2.Server.Cache.Cache("Probes")
    groupcache.expire("probegroups")

    Bcfg2.Server.Cache.expire("Probes", "probegroups")

The former will not expire any data, because there is no single datum
tagged with ``"Probes", "probegroups"``.  The latter will expire *all*
items tagged with ``"Probes", "probegroups"`` -- i.e., the entire
cache.  In this case, the latter call is equivalent to:

.. code-block:: python

    groupcache = Bcfg2.Server.Cache.Cache("Probes", "probegroups")
    groupcache.expire()

"""

from Bcfg2.Compat import MutableMapping


class _Cache(MutableMapping):
    """ The object returned by :func:`Bcfg2.Server.Cache.Cache` that
    presents a dict-like interface to the portion of the unified cache
    that uses the specified tags. """
    def __init__(self, registry, tags):
        self._registry = registry
        self._tags = tags

    def __getitem__(self, key):
        return self._registry[self._tags | set([key])]

    def __setitem__(self, key, value):
        self._registry[self._tags | set([key])] = value

    def __delitem__(self, key):
        del self._registry[self._tags | set([key])]

    def __iter__(self):
        for item in self._registry.iterate(*self._tags):
            yield list(item.difference(self._tags))[0]

    def keys(self):
        """ List cache keys """
        return list(iter(self))

    def __len__(self):
        return len(list(iter(self)))

    def expire(self, key=None):
        """ expire all items, or a specific item, from the cache """
        if key is None:
            expire(*self._tags)
        else:
            tags = self._tags | set([key])
            # py 2.5 doesn't support mixing *args and explicit keyword
            # args
            kwargs = dict(exact=True)
            expire(*tags, **kwargs)

    def __repr__(self):
        return repr(dict(self))

    def __str__(self):
        return str(dict(self))


class _CacheRegistry(dict):
    """ The grand unified cache backend which contains all cache
    items. """

    def iterate(self, *tags):
        """ Iterate over all items that match the given tags *and*
        have exactly one additional tag.  This is used to get items
        for :class:`Bcfg2.Server.Cache._Cache` objects that have been
        instantiated via :func:`Bcfg2.Server.Cache.Cache`. """
        tags = frozenset(tags)
        for key in self.keys():
            if key.issuperset(tags) and len(key.difference(tags)) == 1:
                yield key

    def iter_all(self, *tags):
        """ Iterate over all items that match the given tags,
        regardless of how many additional tags they have (or don't
        have). This is used to expire all cache data that matches a
        set of tags. """
        tags = frozenset(tags)
        for key in list(self.keys()):
            if key.issuperset(tags):
                yield key


_cache = _CacheRegistry()  # pylint: disable=C0103
_hooks = []  # pylint: disable=C0103


[docs]def Cache(*tags): # pylint: disable=C0103 """ A dict interface to the cache data tagged with the given tags. """ return _Cache(_cache, frozenset(tags))
[docs]def expire(*tags, **kwargs): """ Expire all items, a set of items, or one specific item from the cache. If ``exact`` is set to True, then if the given tag set doesn't match exactly one item in the cache, nothing will be expired. """ exact = kwargs.pop("exact", False) count = 0 if not tags: count = len(_cache) _cache.clear() elif exact: if frozenset(tags) in _cache: count = 1 del _cache[frozenset(tags)] else: for match in _cache.iter_all(*tags): count += 1 del _cache[match] for hook in _hooks: hook(tags, exact, count)
[docs]def add_expire_hook(func): """ Add a hook that will be called when an item is expired from the cache. The callable passed in must take three options: the first will be the tag set that was expired; the second will be the state of the ``exact`` flag (True or False); and the third will be the number of items that were expired from the cache. """ _hooks.append(func)