| 1 |
import threading |
|---|
| 2 |
import loggingclass |
|---|
| 3 |
import time |
|---|
| 4 |
|
|---|
| 5 |
class CacheEntry: |
|---|
| 6 |
""" |
|---|
| 7 |
A class representing a cache entry. |
|---|
| 8 |
x = CacheEntry(<some object>) |
|---|
| 9 |
""" |
|---|
| 10 |
|
|---|
| 11 |
def __init__(self, val): |
|---|
| 12 |
self.val = val |
|---|
| 13 |
self.stamp = time.time() |
|---|
| 14 |
|
|---|
| 15 |
def expired(self, period): |
|---|
| 16 |
""" |
|---|
| 17 |
Boolean: returns true if the entry is older than period (in sec). |
|---|
| 18 |
""" |
|---|
| 19 |
return time.time() - self.stamp > period |
|---|
| 20 |
|
|---|
| 21 |
def __cmp__(self, other): |
|---|
| 22 |
""" |
|---|
| 23 |
CacheEntry objects can be compared by age. |
|---|
| 24 |
""" |
|---|
| 25 |
return cmp(self.stamp, other.stamp) |
|---|
| 26 |
|
|---|
| 27 |
|
|---|
| 28 |
class Cache(loggingclass.LoggingClass): |
|---|
| 29 |
""" |
|---|
| 30 |
A simple cache implementation |
|---|
| 31 |
|
|---|
| 32 |
Usage: c = Cache(expire=<expire>, size=<size>, entryclass=CacheEntry) |
|---|
| 33 |
<expire>: expiration time of entries in sec (default: 60) |
|---|
| 34 |
<size>: max number of cache entries (default: 1000) |
|---|
| 35 |
<entryclass>: A class for the cache entries (default: CacheEntry) |
|---|
| 36 |
|
|---|
| 37 |
NOTE: EXPIRED ENTRIES WILL BE DELETED. |
|---|
| 38 |
Do not use this class (exclusively) to store valuable data. |
|---|
| 39 |
|
|---|
| 40 |
Entries are assigned and retrieved through indexing: |
|---|
| 41 |
cache[x] = val |
|---|
| 42 |
try: |
|---|
| 43 |
val = cache[x] |
|---|
| 44 |
except KeyError: |
|---|
| 45 |
print x, ": not in cache" |
|---|
| 46 |
cache.invalidate(x) |
|---|
| 47 |
|
|---|
| 48 |
doctest example: |
|---|
| 49 |
|
|---|
| 50 |
>>> from time import sleep |
|---|
| 51 |
>>> exp=2.0 |
|---|
| 52 |
>>> cache = Cache(size=10, expire=exp) |
|---|
| 53 |
>>> for x in range(0, 10): |
|---|
| 54 |
... cache[x] = x*x |
|---|
| 55 |
>>> print cache.contents() |
|---|
| 56 |
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25), (6, 36), (7, 49), (8, 64), (9, 81)] |
|---|
| 57 |
>>> for x in range(11, 20): |
|---|
| 58 |
... cache[x] = x*x |
|---|
| 59 |
>>> |
|---|
| 60 |
>>> # size exceeded - old elements will be deleted |
|---|
| 61 |
>>> print cache.contents() |
|---|
| 62 |
[(11, 121), (12, 144), (13, 169), (14, 196), (15, 225), (16, 256), (17, 289), (18, 324), (19, 361)] |
|---|
| 63 |
>>> sleep(exp/2.) |
|---|
| 64 |
>>> for x in range(0, 5): |
|---|
| 65 |
... cache[x] = x*x |
|---|
| 66 |
>>> print cache.contents() |
|---|
| 67 |
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (16, 256), (17, 289), (18, 324), (19, 361)] |
|---|
| 68 |
>>> sleep(exp/2.+0.1) |
|---|
| 69 |
>>> |
|---|
| 70 |
>>> # elements 11 .. 20 will be expired |
|---|
| 71 |
>>> print cache.contents() |
|---|
| 72 |
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16)] |
|---|
| 73 |
""" |
|---|
| 74 |
|
|---|
| 75 |
default_expire = 60 |
|---|
| 76 |
default_size = 1000 |
|---|
| 77 |
_to_shrink = 100 |
|---|
| 78 |
|
|---|
| 79 |
def __init__(self, expire = default_expire, size = default_size, |
|---|
| 80 |
entryclass = CacheEntry): |
|---|
| 81 |
self.__cache = {} |
|---|
| 82 |
self.__size = size |
|---|
| 83 |
self.expire = expire |
|---|
| 84 |
self.__lock = threading.Lock() |
|---|
| 85 |
self._entryclass = entryclass |
|---|
| 86 |
|
|---|
| 87 |
def _lock(self): |
|---|
| 88 |
self.__lock.acquire() |
|---|
| 89 |
|
|---|
| 90 |
def _unlock(self): |
|---|
| 91 |
self.__lock.release() |
|---|
| 92 |
|
|---|
| 93 |
def _invalidate(self, key): |
|---|
| 94 |
if self.__cache.has_key(key): |
|---|
| 95 |
del self.__cache[key] |
|---|
| 96 |
return True |
|---|
| 97 |
return False |
|---|
| 98 |
|
|---|
| 99 |
def len(self): |
|---|
| 100 |
""" |
|---|
| 101 |
Return current number of cache entries. |
|---|
| 102 |
""" |
|---|
| 103 |
return len(self.__cache) |
|---|
| 104 |
|
|---|
| 105 |
def invalidate(self, key): |
|---|
| 106 |
""" |
|---|
| 107 |
Invalidate (delete) cache entry indexed by key |
|---|
| 108 |
""" |
|---|
| 109 |
self._lock() |
|---|
| 110 |
try: |
|---|
| 111 |
if self._invalidate(key): |
|---|
| 112 |
self.logger.debug("element %s invalidated" % key) |
|---|
| 113 |
finally: |
|---|
| 114 |
self._unlock() |
|---|
| 115 |
|
|---|
| 116 |
def invalidate_all(self): |
|---|
| 117 |
""" |
|---|
| 118 |
Clear cache completetly |
|---|
| 119 |
""" |
|---|
| 120 |
self._lock() |
|---|
| 121 |
try: |
|---|
| 122 |
self.__cache.clear() |
|---|
| 123 |
finally: |
|---|
| 124 |
self._unlock() |
|---|
| 125 |
self.logger.info("cache cleared") |
|---|
| 126 |
|
|---|
| 127 |
def invalidate_some(self, func): |
|---|
| 128 |
""" |
|---|
| 129 |
Invalidate all entries for which func(key,val) returns True |
|---|
| 130 |
""" |
|---|
| 131 |
self._lock() |
|---|
| 132 |
try: |
|---|
| 133 |
for x in self.__cache.keys(): |
|---|
| 134 |
if func(x, self.__cache[x].val): |
|---|
| 135 |
self._invalidate(x) |
|---|
| 136 |
self.logger.debug("element %s invalidated" % x) |
|---|
| 137 |
finally: |
|---|
| 138 |
self._unlock() |
|---|
| 139 |
|
|---|
| 140 |
def _expired(self, entry): |
|---|
| 141 |
return self.expire != 0 and entry.expired(self.expire) |
|---|
| 142 |
|
|---|
| 143 |
def __getitem__(self, key): |
|---|
| 144 |
""" |
|---|
| 145 |
Implements x = cache[key]. |
|---|
| 146 |
Will raise KeyError if the cache entry is expired. |
|---|
| 147 |
""" |
|---|
| 148 |
ret = None |
|---|
| 149 |
self._lock() |
|---|
| 150 |
try: |
|---|
| 151 |
entry = self.__cache[key] |
|---|
| 152 |
if self._expired(entry): |
|---|
| 153 |
self._gc() |
|---|
| 154 |
raise KeyError |
|---|
| 155 |
else: |
|---|
| 156 |
ret = entry.val |
|---|
| 157 |
finally: |
|---|
| 158 |
self._unlock() |
|---|
| 159 |
|
|---|
| 160 |
return ret |
|---|
| 161 |
|
|---|
| 162 |
def __setitem__(self, key, val): |
|---|
| 163 |
|
|---|
| 164 |
""" |
|---|
| 165 |
Implements cache[key] = y. |
|---|
| 166 |
""" |
|---|
| 167 |
self._lock() |
|---|
| 168 |
try: |
|---|
| 169 |
self._invalidate(key) |
|---|
| 170 |
if self.len() >= self.__size: |
|---|
| 171 |
self._shrink() |
|---|
| 172 |
self.__cache[key] = self._entryclass(val) |
|---|
| 173 |
finally: |
|---|
| 174 |
self._unlock() |
|---|
| 175 |
|
|---|
| 176 |
def _shrink(self): |
|---|
| 177 |
|
|---|
| 178 |
target = self.__size - min(self.__size/2, self._to_shrink) |
|---|
| 179 |
self.logger.debug("_shrink: trying to reduce size from %d to %d" |
|---|
| 180 |
% (self.len(), target)) |
|---|
| 181 |
|
|---|
| 182 |
self._gc() |
|---|
| 183 |
|
|---|
| 184 |
n = self.len() - target |
|---|
| 185 |
if n <= 0: |
|---|
| 186 |
return |
|---|
| 187 |
|
|---|
| 188 |
|
|---|
| 189 |
keys = self.__cache.keys() |
|---|
| 190 |
keys.sort(lambda a, b, c=self.__cache: cmp(c[a], c[b])) |
|---|
| 191 |
|
|---|
| 192 |
self.logger.info("_shrink: invalidating %d entries" % n) |
|---|
| 193 |
for key in keys[:n]: |
|---|
| 194 |
self._invalidate(key) |
|---|
| 195 |
|
|---|
| 196 |
def _gc(self): |
|---|
| 197 |
|
|---|
| 198 |
expired = [] |
|---|
| 199 |
for key in self.__cache.keys(): |
|---|
| 200 |
if self._expired(self.__cache[key]): |
|---|
| 201 |
expired.append(key) |
|---|
| 202 |
for key in expired: |
|---|
| 203 |
self._invalidate(key) |
|---|
| 204 |
self.logger.info("collecting garbage: %d items invalidated" |
|---|
| 205 |
% len(expired)) |
|---|
| 206 |
|
|---|
| 207 |
def contents(self): |
|---|
| 208 |
""" |
|---|
| 209 |
returns a list of (key, val) tuples for all non-expired entries |
|---|
| 210 |
""" |
|---|
| 211 |
self._lock() |
|---|
| 212 |
try: |
|---|
| 213 |
self._gc() |
|---|
| 214 |
ret = [(x, self.__cache[x].val) for x in self.__cache.keys()] |
|---|
| 215 |
finally: |
|---|
| 216 |
self._unlock() |
|---|
| 217 |
return ret |
|---|
| 218 |
|
|---|
| 219 |
|
|---|
| 220 |
def _test(): |
|---|
| 221 |
import doctest, simplecache |
|---|
| 222 |
doctest.testmod(simplecache) |
|---|
| 223 |
|
|---|
| 224 |
if __name__ == "__main__": |
|---|
| 225 |
_test() |
|---|