Added cache_utils to the distribution because it includes a patch and is not packaged...
[plcapi.git] / cache_utils / group_backend.py
diff --git a/cache_utils/group_backend.py b/cache_utils/group_backend.py
new file mode 100644 (file)
index 0000000..8377477
--- /dev/null
@@ -0,0 +1,110 @@
+"""
+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
+from cache_utils.utils import sanitize_memcached_key
+
+# 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 sanitize_memcached_key(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