import threading from weakref import ref from time import time as now True, False = 1==1, 0==1 class CacheFactory(object): """ CacheFactory caches object creation. Each object should be referenced by a single hashable ID (note tuples of hashable values are also hashable). """ def __init__(self, cullFrequency=100, cullFraction=2, cache=True): """ Every cullFrequency times that an item is retrieved from this cache, the cull method is called. The cull method then expires an arbitrary fraction of the cached objects. The idea is at no time will the cache be entirely emptied, placing a potentially high load at that moment, but everything object will have its time to go eventually. The fraction is given as an integer, and one in that many objects are expired (i.e., the default is 1/2 of objects are expired). By setting cache to False, items won't be cached. However, in all cases a weak reference is kept to created objects, and if the object hasn't been garbage collected it will be returned. """ self.cullFrequency = cullFrequency self.cullCount = cullFrequency self.cullOffset = 0 self.cullFraction = cullFraction self.doCache = cache if self.doCache: self.cache = {} self.expiredCache = {} self.lock = threading.Lock() def tryGet(self, id): return self.cache.get(id) def get(self, id): if self.doCache: if self.cullCount > self.cullFrequency: # Two threads could hit the cull in a row, but # that's not so bad. At least by setting cullCount # back to zero right away we avoid this. The cull # method has a lock, so it's threadsafe. self.cullCount = 0 self.cull() try: return self.cache[id] except KeyError: pass self.lock.acquire() try: val = self.cache[id] except KeyError: pass else: self.lock.release() return val try: val = self.expiredCache[id]() except KeyError: return None else: del self.expiredCache[id] if val is None: return None self.cache[id] = val self.lock.release() return val else: try: val = self.expiredCache[id]() if val is not None: return val except KeyError: pass self.lock.acquire() try: val = self.expiredCache[id]() except KeyError: return None else: if val is None: del self.expiredCache[id] return None self.lock.release() return val def put(self, id, obj): if self.doCache: self.cache[id] = obj else: self.expiredCache[id] = ref(obj) def finishPut(self): self.lock.release() def created(self, id, obj): if self.doCache: self.cache[id] = obj else: self.expiredCache[id] = ref(obj) def cull(self): self.lock.acquire() try: keys = self.cache.keys() for i in xrange(self.cullOffset, len(keys), self.cullFraction): id = keys[i] self.expiredCache[id] = ref(self.cache[id]) del self.cache[id] # This offset tries to balance out which objects we # expire, so no object will just hang out in the cache # forever. self.cullOffset = (self.culldOffset + 1) % self.cullFraction finally: self.lock.release() def clear(self): self.cache.clear() self.expiredCache.clear() def expire(self, id): if not self.doCache: return self.lock.acquire() try: if self.cache.has_key(id): del self.cache[id] if self.expiredCache.has_key(id): del self.expiredCache[id] finally: self.lock.release() def expireAll(self): if not self.doCache: return self.lock.acquire() try: for key, value in self.cache.items(): self.expiredCache[key] = ref(obj) self.cache = {} finally: self.lock.release() def allIDs(self): if self.doCache: all = self.cache.keys() else: all = [] for id, value in self.expiredCache: if value(): all.append(id) return all class CacheSet(object): def __init__(self, *args, **kw): self.caches = {} self.args = args self.kw = kw def get(self, id, cls): try: return self.caches[cls.__name__].get(id) except KeyError: self.caches[cls.__name__] = CacheFactory(*self.args, **self.kw) return self.caches[cls.__name__].get(id) def put(self, id, cls, obj): self.caches[cls.__name__].put(id, obj) def finishPut(self, cls): self.caches[cls.__name__].finishPut() def created(self, id, cls, obj): try: self.caches[cls.__name__].created(id, obj) except KeyError: self.caches[cls.__name__] = CacheFactory(*self.args, **self.kw) self.caches[cls.__name__].created(id, obj) def expire(self, id, cls): try: self.caches[cls.__name__].expire(id) except KeyError: pass def clear(self, cls=None): if cls is None: for cache in self.caches.values(): cache.clear() elif self.caches.has_key(cls.__name__): self.caches[cls.__name__].clear() def tryGet(self, id, cls): try: self.caches[cls.__name__].tryGet(id) except KeyError: return None def allIDs(self, cls): try: self.caches[cls.__name__].allIDs() except KeyError: return [] def allSubCaches(self): return self.caches.values()