Source

django-cache-utils / cache_utils / group_backend.py

Full commit
"""
Memcached cache backend with group O(1) invalidation ability, dog-pile
effect prevention using MintCache algorythm and project version support to allow
gracefull updates and multiple django projects on same memcached instance.
Long keys (>250) are truncated and appended with md5 hash.
"""

import uuid
import logging
import sys
import time
from django.core.cache.backends.memcached import CacheClass as MemcachedCacheClass
from django.conf import settings

# This prefix is appended to the group name to prevent cache key clashes.
_VERSION_PREFIX = getattr(settings, 'VERSION', "")
_KEY_PREFIX = "_group::"

# MINT_DELAY is an upper bound on how long any value should take to
# be generated (in seconds)
MINT_DELAY = 30

class CacheClass(MemcachedCacheClass):

    def add(self, key, value, timeout=0, group=None):
        key = self._make_key(group, key)

        refresh_time = timeout + time.time()
        real_timeout = timeout + MINT_DELAY
        packed_value = (value, refresh_time, False)

        return super(CacheClass, self).add(key, packed_value, real_timeout)

    def get(self, key, default=None, group=None):
        key = self._make_key(group, key)
        packed_value = super(CacheClass, self).get(key, default)
        if packed_value is None:
            return default
        value, refresh_time, refreshed = packed_value
        if (time.time() > refresh_time) and not refreshed:
            # Store the stale value while the cache revalidates for another
            # MINT_DELAY seconds.
            self.set(key, value, timeout=MINT_DELAY, group=group, refreshed=True)
            return default
        return value

    def set(self, key, value, timeout=0, group=None, refreshed=False):
        key = self._make_key(group, key)
        refresh_time = timeout + time.time()
        real_timeout = timeout + MINT_DELAY
        packed_value = (value, refresh_time, refreshed)
        return super(CacheClass, self).set(key, packed_value, real_timeout)

    def delete(self, key, group=None):
        key = self._make_key(group, key)
        return super(CacheClass, self).delete(key)

    def invalidate_group(self, group):
        """ Invalidates all cache keys belonging to group """
        key = "%s%s%s" % (_VERSION_PREFIX, _KEY_PREFIX, group)
        super(CacheClass, self).delete(key)

    def _make_key(self, group, key, hashkey=None):
        """ Generates a new cache key which belongs to a group, has
            _VERSION_PREFIX prepended and is shorter than memcached key length
            limit.
        """
        key = _VERSION_PREFIX + key
        if group:
            if not hashkey:
                hashkey = self._get_hashkey(group)
            key = "%s:%s-%s" % (group, key, hashkey)
        return key

    def _get_hashkey(self, group):
        """ This can be useful sometimes if you're doing a very large number
            of operations and you want to avoid all of the extra cache hits.
        """
        key = "%s%s%s" % (_VERSION_PREFIX, _KEY_PREFIX, group)
        hashkey = super(CacheClass, self).get(key)
        if hashkey is None:
            hashkey = str(uuid.uuid4())
            super(CacheClass, self).set(key, hashkey)
        return hashkey

    def clear(self):
        self._cache.flush_all()

# ======================================
# I didn't implement methods below to work with MintCache so raise
# NotImplementedError for them.

    def incr(self, key, delta=1, group=None):
#        if group:
#            key = self._make_key(group, key)
#        return super(CacheClass, self).incr(key, delta)
        raise NotImplementedError

    def decr(self, key, delta=1, group=None):
#        if group:
#            key = self._make_key(group, key)
#        return super(CacheClass, self).decr(key, delta)
        raise NotImplementedError

    def get_many(self, keys, group=None):
#        hashkey = self._get_hashkey(group)
#        keys = [self._make_key(group, k, hashkey) for k in keys]
#        return super(CacheClass, self).get_many(keys)
        raise NotImplementedError