Add expiry seconds option to cacheclass.

master
voussoir 2021-01-01 16:24:22 -08:00
parent 593dfe5cb0
commit 166ed18004
1 changed files with 61 additions and 7 deletions

View File

@ -1,42 +1,96 @@
import collections import collections
import time
from voussoirkit import sentinel
NO_ITEM = sentinel.Sentinel('no item')
class Cache: class Cache:
def __init__(self, maxlen): def __init__(self, maxlen, expiry=float('inf')):
self.maxlen = maxlen self.maxlen = maxlen
self.expiry = expiry
self.cache = collections.OrderedDict() self.cache = collections.OrderedDict()
# To prevent excessive purge loops during repeated setitem, only allow
# a purge once every this many seconds.
self.max_purge_frequency = 0.5
self._last_purge = 0
def __contains__(self, key): def __contains__(self, key):
return key in self.cache return self.get(key, fallback=NO_ITEM) is not NO_ITEM
def __getitem__(self, key): def __getitem__(self, key):
value = self.cache.pop(key) '''
self.cache[key] = value Return the key's value, or raise KeyError.
'''
# Let KeyError raise to caller.
(value, timestamp) = self.cache.pop(key)
now = time.time()
if (now - timestamp) > self.expiry:
raise KeyError(key)
self.cache[key] = (value, timestamp)
return value return value
def __len__(self): def __len__(self):
'''
Purge expired items, then count the length.
Due to the purge, this method is not O(1) as most len methods are.
'''
self._purge_expired()
return len(self.cache) return len(self.cache)
def __setitem__(self, key, value): def __setitem__(self, key, value):
# If the key was already present, we don't need to worry about maxlen
# because the net change is zero. If it was not present (KeyError) we
# check the maxlen and pop the oldest item if needed.
# Either way we update the timestamp.
try: try:
self.cache.pop(key) self.cache.pop(key)
except KeyError: except KeyError:
if len(self.cache) >= self.maxlen: if len(self) >= self.maxlen:
self.cache.popitem(last=False) self.cache.popitem(last=False)
self.cache[key] = value self.cache[key] = (value, time.time())
def _purge_expired(self):
now = time.time()
if now - self._last_purge < self.max_purge_frequency:
return
for (key, (value, timestamp)) in list(self.cache.items()):
if (now - timestamp) > self.expiry:
self.cache.pop(key)
self._last_purge = now
def clear(self): def clear(self):
'''
Remove everything from the cache.
'''
self.cache.clear() self.cache.clear()
def get(self, key, fallback=None): def get(self, key, fallback=None):
'''
Return the key's value, or fallback in case of KeyError.
'''
try: try:
return self[key] return self[key]
except KeyError: except KeyError:
return fallback return fallback
def pop(self, key): def pop(self, key):
return self.cache.pop(key) '''
Remove the key and return its value, or raise KeyError.
'''
(value, timestamp) = self.cache.pop(key)
return value
def remove(self, key): def remove(self, key):
'''
Remove the item and ignore KeyError.
'''
try: try:
self.pop(key) self.pop(key)
except KeyError: except KeyError: