summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Cache.py
blob: d05eb0bf6ee2d48eb7f2f57e43e67d6648865a34 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
""" ``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


def Cache(*tags):  # pylint: disable=C0103
    """ A dict interface to the cache data tagged with the given
    tags. """
    return _Cache(_cache, frozenset(tags))


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)


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)