class _BoundedCacheSet: # implements LRU. I could've implemented this using # a set and then removed a random item from the set whenever the set # got too large. Hmmm... def __init__(self, max_items): assert max_items > 1 self._max_items = max_items self._data = IndexedMultiMap() # recent accesses. def add(self, key): # O(log n) i = self._data.find_key_by_value(key) t = time() if i.at_end(): self._data[t] = key else: self._data.update_key(i,t) while len(self._data) > self._max_items: j = self._data.begin() assert not j.at_end() self._data.erase(j) def __contains__(self, key): i = self._data.find_key_by_value(key) return not i.at_end() def remove(self, key): i = self._data.find_key_by_value(key) if i.at_end(): raise KeyError() self._data.erase(i) def __str__(self): return str(self._data)
def __init__(self, default_ttl=None, expire_interval=60, touch_on_access=False, max_items=None, recent_items=RECENT_SIZE): """ @param default_ttl: time to live when using __setitem__ rather than set. @param expire_interval: time between removals of expired items in seconds. Otherwise, expired items are removed lazily. @param touch_on_access: refresh item expire time by ttl when item is accessed. @param max_items: maximum size of cache. (see replacement algorithm above) """ self._exp = IndexedMultiMap( ) # expiration times. Multiple items can have the same expiration # times, but there can only be one instance of any one key # in the CacheMap. self._data = {} self._ttl = default_ttl self._touch = touch_on_access self._max_items = max_items self._expire_interval = expire_interval if max_items is not None: self._recent = _BoundedCacheSet(int(min(recent_items, max_items))) else: self._recent = None reactor.callLater(self._expire_interval, self._expire)
class _BoundedCacheSet: # implements LRU. I could've implemented this using # a set and then removed a random item from the set whenever the set # got too large. Hmmm... def __init__(self, max_items): assert max_items > 1 self._max_items = max_items self._data = IndexedMultiMap() # recent accesses. def add(self, key): # O(log n) i = self._data.find_key_by_value(key) t = time() if i.at_end(): self._data[t] = key else: self._data.update_key(i, t) while len(self._data) > self._max_items: j = self._data.begin() assert not j.at_end() self._data.erase(j) def __contains__(self, key): i = self._data.find_key_by_value(key) return not i.at_end() def remove(self, key): i = self._data.find_key_by_value(key) if i.at_end(): raise KeyError() self._data.erase(i) def __str__(self): return str(self._data)
def __init__(self, default_ttl = None, expire_interval = 60, touch_on_access = False, max_items = None, recent_items = RECENT_SIZE ): """ @param default_ttl: time to live when using __setitem__ rather than set. @param expire_interval: time between removals of expired items in seconds. Otherwise, expired items are removed lazily. @param touch_on_access: refresh item expire time by ttl when item is accessed. @param max_items: maximum size of cache. (see replacement algorithm above) """ self._exp = IndexedMultiMap() # expiration times. Multiple items can have the same expiration # times, but there can only be one instance of any one key # in the CacheMap. self._data = {} self._ttl = default_ttl self._touch = touch_on_access self._max_items = max_items self._expire_interval = expire_interval if max_items is not None: self._recent = _BoundedCacheSet(int(min(recent_items,max_items))) else: self._recent = None reactor.callLater(self._expire_interval, self._expire)
class CacheMap: """this cache class allows caching with arbitrary expiration times. This is different from BTL.Cache which assumes all items placed in the cache remain valid for the same duration. Like a BTL.Map, there can only be one instance of any given key in the cache at a time. Subsequent inserts (__setitem__ or set) with the same key, will update the ttl and value for that key. Unlike a BTL.Map, CacheMap does not perform in-order iteration based on key, and key lookups (__getitem__) take average O(1) time. The map also has the option to have bounded size in which case it imposes the following replacement algorithm: remove the oldest entries first unless those entries are in the recent access set. Here 'old' refers to duration in the cache. Recent set has bounded size. """ # BTL.Cache places the cache entries in a queue. We instead maintain an # IndexedMultiMap ordered based on expiration times. The index allows nodes in the # map to be looked up in O(1) time based on value. def __init__(self, default_ttl = None, expire_interval = 60, touch_on_access = False, max_items = None, recent_items = RECENT_SIZE ): """ @param default_ttl: time to live when using __setitem__ rather than set. @param expire_interval: time between removals of expired items in seconds. Otherwise, expired items are removed lazily. @param touch_on_access: refresh item expire time by ttl when item is accessed. @param max_items: maximum size of cache. (see replacement algorithm above) """ self._exp = IndexedMultiMap() # expiration times. Multiple items can have the same expiration # times, but there can only be one instance of any one key # in the CacheMap. self._data = {} self._ttl = default_ttl self._touch = touch_on_access self._max_items = max_items self._expire_interval = expire_interval if max_items is not None: self._recent = _BoundedCacheSet(int(min(recent_items,max_items))) else: self._recent = None reactor.callLater(self._expire_interval, self._expire) def __getitem__(self, key): # O(1) if not touch and not newly expired, else O(log n) """Raises KeyError if the key is not in the cache. This can happen if the entry was deleted or expired.""" ttl,v = self._data[key] # ttl is duration, not absolute time. i = self._exp.find_key_by_value(key) # O(1). Key in exp is time. 'key' variable # is exp's value. :-) if i.at_end(): raise KeyError() t = time() if i.key() < t: # expired. del self[key] # O(log n) raise KeyError() if self._recent: self._recent.add(key) if self._touch: self._exp.update_key(i,t+ttl) # O(1) if no reordering else O(log n) return v def __setitem__(self, key, value): # O(log n). actually O(log n + RECENT_SIZE) assert self._ttl > 0, "no default TTL defined. Perhaps the caller should call set " \ "rather than __setitem__." t = time() if self._data.has_key(key): ttl,_ = self._data[key] else: ttl = self._ttl self.set(key,value,ttl) if self._recent: self._recent.add(key) # perform cache replacement if necessary. if self._max_items is not None and len(self._data) > self._max_items: to_remove = [] for t,k in self._exp.iteritems(): # worst case is O(RECENT_SIZE), but it is highly unlikely # that all members of the recent access set are the oldest # in the cache. if k not in self._recent: to_remove.append(k) if len(to_remove) >= len(self._data) - self._max_items: break for k in to_remove: del self[k] def set(self, key, value, ttl): """Set using non-default TTL. ttl is a duration, not an absolute time.""" t = time() self._data[key] = (ttl, value) i = self._exp.find_key_by_value(key) if i.at_end(): self._exp[t+ttl] = key else: assert i.value() == key self._exp.update_key(i,t+ttl) def __delitem__(self, key): # O(log n) del self._data[key] i = self._exp.find_key_by_value(key) if not i.at_end(): # No KeyError is generated if item is not in # Cache because it could've been expired. self._exp.erase(i) def __len__(self): """Returns number of entries in the cache. Includes any expired entries that haven't been removed yet. Takes O(1) time.""" return len(self._data) def num_unexpired(self): """Returns number of unexpired entries in the cache. Any expired entries are removed before computing the length. Takes worst case O(n) time where n = the number of expired entries in the cache when this is called.""" self._expire2() return len(self._data) def has_key(self, key): return self._data.has_key(key) def __contains__(self, key): return self._data.has_key(key) def keys(self): return self._data.keys() def _expire(self): self._expire2() reactor.callLater(self._expire_interval, self._expire) def _expire2(self): t = time() #try: while True: i = self._exp.begin() if i.at_end(): break if i.key() < t: key = i.value() self._exp.erase(i) del self._data[key] else: break assert len(self._data) == len(self._exp)
def __init__(self, max_items): assert max_items > 1 self._max_items = max_items self._data = IndexedMultiMap() # recent accesses.
class CacheMap: """this cache class allows caching with arbitrary expiration times. This is different from BTL.Cache which assumes all items placed in the cache remain valid for the same duration. Like a BTL.Map, there can only be one instance of any given key in the cache at a time. Subsequent inserts (__setitem__ or set) with the same key, will update the ttl and value for that key. Unlike a BTL.Map, CacheMap does not perform in-order iteration based on key, and key lookups (__getitem__) take average O(1) time. The map also has the option to have bounded size in which case it imposes the following replacement algorithm: remove the oldest entries first unless those entries are in the recent access set. Here 'old' refers to duration in the cache. Recent set has bounded size. """ # BTL.Cache places the cache entries in a queue. We instead maintain an # IndexedMultiMap ordered based on expiration times. The index allows nodes in the # map to be looked up in O(1) time based on value. def __init__(self, default_ttl=None, expire_interval=60, touch_on_access=False, max_items=None, recent_items=RECENT_SIZE): """ @param default_ttl: time to live when using __setitem__ rather than set. @param expire_interval: time between removals of expired items in seconds. Otherwise, expired items are removed lazily. @param touch_on_access: refresh item expire time by ttl when item is accessed. @param max_items: maximum size of cache. (see replacement algorithm above) """ self._exp = IndexedMultiMap( ) # expiration times. Multiple items can have the same expiration # times, but there can only be one instance of any one key # in the CacheMap. self._data = {} self._ttl = default_ttl self._touch = touch_on_access self._max_items = max_items self._expire_interval = expire_interval if max_items is not None: self._recent = _BoundedCacheSet(int(min(recent_items, max_items))) else: self._recent = None reactor.callLater(self._expire_interval, self._expire) def __getitem__( self, key): # O(1) if not touch and not newly expired, else O(log n) """Raises KeyError if the key is not in the cache. This can happen if the entry was deleted or expired.""" ttl, v = self._data[key] # ttl is duration, not absolute time. i = self._exp.find_key_by_value( key) # O(1). Key in exp is time. 'key' variable # is exp's value. :-) if i.at_end(): raise KeyError() t = time() if i.key() < t: # expired. del self[key] # O(log n) raise KeyError() if self._recent: self._recent.add(key) if self._touch: self._exp.update_key(i, t + ttl) # O(1) if no reordering else O(log n) return v def __setitem__(self, key, value): # O(log n). actually O(log n + RECENT_SIZE) assert self._ttl > 0, "no default TTL defined. Perhaps the caller should call set " \ "rather than __setitem__." t = time() if self._data.has_key(key): ttl, _ = self._data[key] else: ttl = self._ttl self.set(key, value, ttl) if self._recent: self._recent.add(key) # perform cache replacement if necessary. if self._max_items is not None and len(self._data) > self._max_items: to_remove = [] for t, k in self._exp.iteritems(): # worst case is O(RECENT_SIZE), but it is highly unlikely # that all members of the recent access set are the oldest # in the cache. if k not in self._recent: to_remove.append(k) if len(to_remove) >= len(self._data) - self._max_items: break for k in to_remove: del self[k] def set(self, key, value, ttl): """Set using non-default TTL. ttl is a duration, not an absolute time.""" t = time() self._data[key] = (ttl, value) i = self._exp.find_key_by_value(key) if i.at_end(): self._exp[t + ttl] = key else: assert i.value() == key self._exp.update_key(i, t + ttl) def __delitem__(self, key): # O(log n) del self._data[key] i = self._exp.find_key_by_value(key) if not i.at_end(): # No KeyError is generated if item is not in # Cache because it could've been expired. self._exp.erase(i) def __len__(self): """Returns number of entries in the cache. Includes any expired entries that haven't been removed yet. Takes O(1) time.""" return len(self._data) def num_unexpired(self): """Returns number of unexpired entries in the cache. Any expired entries are removed before computing the length. Takes worst case O(n) time where n = the number of expired entries in the cache when this is called.""" self._expire2() return len(self._data) def has_key(self, key): return self._data.has_key(key) def __contains__(self, key): return self._data.has_key(key) def keys(self): return self._data.keys() def _expire(self): self._expire2() reactor.callLater(self._expire_interval, self._expire) def _expire2(self): t = time() #try: while True: i = self._exp.begin() if i.at_end(): break if i.key() < t: key = i.value() self._exp.erase(i) del self._data[key] else: break assert len(self._data) == len(self._exp)