# Based on http://djangosnippets.org/snippets/564/
# Modified to stagger cache durations
# Modified to prevent cache stampeding (on cache miss, inserts a placeholder while regenerating a hit)
# Modified to allow correct storage of None results without triggering cache expiry
# Modified to incorporate object updated attributes to help expire keys
from django.core.cache import cache
from django.db.models import Model
from hashlib import sha1
from random import randint
import math
def cache_result(seconds=3600, expiry_variance=0.2, override_key=None):
def doCache(f):
def x(*args, **kwargs):
# Generate the key from the function name and given arguments
key = sha1(override_key or u"//".join((
unicode(f.__module__),
unicode(args[0].__class__.__name__) if args else "",
unicode(f.__name__),
u"//".join(objectToString(a) for a in args),
u"//".join(unicode(a.updated) for a in args if hasattr(a, "updated")),
u"//".join(unicode(k) + objectToString(v) for k, v in kwargs.iteritems()),
u"//".join(unicode(v.updated) for k, v in kwargs.iteritems() if hasattr(v, "updated")),
)).encode("utf-8")).hexdigest()
flag = key + "flag"
# If a cached result exists, return it.
result = cache.get(key) if cache.get(flag) else None
if result:
return result[0]
# If no result exists, generate one and return it. While generating a result,
# postpone further regenerations to prevent cache stampeding.
cache.set(flag, True, int(math.log(seconds)))
result = f(*args, **kwargs)
cache.set(flag, True, seconds + randint(-int(seconds * expiry_variance), int(seconds * expiry_variance)))
cache.set(key, (result, ), max(seconds * 10, 86400 * 7))
return result
return x
def objectToString(obj):
if isinstance(obj, Model):
object_class = type(obj)
return ".".join((
object_class.__module__,
object_class.__name__,
unicode(obj.pk)
))
else:
return unicode(obj)
return doCache
Comments