I needed a simple expiring in-memory cache module for a project I'm working on and I've come up with the following.
My requirements for the cache module are:
- Be able to expire objects after certain period of time
- Use what we have in the standard library
- Keep it simple
So far I've got this.
"""
Expiring in-memory cache module
"""
import logging
import threading
from time import time
from collections import OrderedDict
__all__ = ['CachedObject', 'CacheInventory', 'CacheException']
class CacheException(Exception):
"""
Generic cache exception
"""
pass
class CachedObject(object):
def __init__(self, name, obj, ttl):
"""
Initializes a new cached object
Args:
name (str): Human readable name for the cached entry
obj (type): Object to be cached
ttl (int): The TTL in seconds for the cached object
"""
self.hits = 0
self.name = name
self.obj = obj
self.ttl = ttl
self.timestamp = time()
class CacheInventory(object):
"""
Inventory for cached objects
"""
def __init__(self, maxsize=0, housekeeping=0):
"""
Initializes a new cache inventory
Args:
maxsize (int): Upperbound limit on the number of items
that will be stored in the cache inventory
housekeeping (int): Time in minutes to perform periodic
cache housekeeping
"""
if maxsize < 0:
raise CacheException('Cache inventory size cannot be negative')
if housekeeping < 0:
raise CacheException('Cache housekeeping period cannot be negative')
self._cache = OrderedDict()
self.maxsize = maxsize
self.housekeeping = housekeeping * 60.0
self.lock = threading.RLock()
if self.housekeeping > 0:
threading.Timer(self.housekeeping, self.housekeeper).start()
def __len__(self):
with self.lock:
return len(self._cache)
def __contains__(self, key):
with self.lock:
if key not in self._cache:
return False
item = self._cache[key]
if self._has_expired(item):
return False
return True
def _has_expired(self, item):
"""
Checks if a cached item has expired and removes it if needed
If the upperbound limit has been reached then the last item
is being removed from the inventory.
Args:
item (CachedObject): A cached object to lookup
"""
with self.lock:
if time() > item.timestamp + item.ttl:
logging.debug(
'Object %s has expired and will be removed from cache [hits %d]',
item.name,
item.hits
)
self._cache.pop(item.name)
return True
return False
def add(self, obj):
"""
Add an item to the cache inventory
Args:
obj (CachedObject): A CachedObject instance to be added
Raises:
CacheException
"""
if not isinstance(obj, CachedObject):
raise Exception('Need a CachedObject instance to add in the cache')
with self.lock:
if self.maxsize > 0 and len(self._cache) == self.maxsize:
popped = self._cache.popitem(last=False)
logging.debug('Cache maxsize reached, removing %s [hits %d]', popped.name, popped.hits)
logging.debug('Caching object %s [ttl: %d seconds]', obj.name, obj.ttl)
self._cache[obj.name] = obj
def get(self, key):
"""
Retrieve an object from the cache inventory
Args:
key (str): Name of the cache item to retrieve
Returns:
The cached object if found, None otherwise
"""
with self.lock:
if key not in self._cache:
return None
item = self._cache[key]
if self._has_expired(item):
return None
item.hits += 1
logging.debug(
'Returning object %s from cache [hits %d]',
item.name,
item.hits
)
return item.obj
def housekeeper(self):
"""
Remove expired entries from the cache on regular basis
"""
with self.lock:
expired = 0
logging.info(
'Starting cache housekeeper [%d items in cache]',
len(self._cache)
)
for name, item in self._cache.items():
if self._has_expired(item):
expired += 1
logging.info(
'Cache housekeeper completed [%d removed from cache]',
expired
)
if self.housekeeping > 0:
threading.Timer(self.housekeeping, self.housekeeper).start()
Here's an example usage of the caching module:
>>> from __future__ import print_function
>>> from __future__ import absolute_import
>>> from . import CachedObject
>>> from . import CacheInventory
>>> cache = CacheInventory(housekeeping=60) # housekeeper will run every 60 minutes
>>> obj = {'key1': 'value1', 'key2': 'value2'}
>>> cached_obj = CachedObject(name='mydictionary', obj=obj, ttl=60) # object will expire in 60 seconds
>>> cache.add(obj=cached_obj)
>>> print(cache.get('mydictionary'))
{'key2': 'value2', 'key1': 'value1'}
>>> # 60 seconds later -> the object has expired already
...
>>> print(cache.get('mydictionary'))
None
I'm currently using this caching module for storing VMware vSphere managed objects and the code can also be found in Github:
Any thoughts, remarks or suggestions about the design and implementation of this caching module?
Note: Code works in both Python 2 and Python 3.