Improving Django Cache – Part III

We’ve setup a custom cache backend and modified it to fix cache keys. Now, we are going to tackle a much more complicated problem; dogpiling.

Dogpiling occurs when an entry in the cache expires. If you get multiple requests for that cached item before it’s new value can be calculated, you end up with attempts from each of those requests to refresh the cache. To prevent this, we are going to work from the MintCache implementation that stores slightly stale data to pass on to requests while the first request for expired data calculates the new value to store in the cache.

The magic to make this happen is in two methods added to the CacheClass, pack_values and examine_values. The pack_values method is used to pack the necessary data so it can be stored in the cache and examine_value is used when data is retrieved from the cache to see if it is expired.

    def pack_values(self, value, timeout, refreshed=False):
        refresh_time = (timeout or self.default_timeout) + time.time()
        real_timeout = (timeout or self.default_timeout) + 30
        stored_value = value
        if isinstance(value, unicode):
            stored_value = value.encode('utf-8')
        return (real_timeout, (stored_value, refresh_time, refreshed))
    def examine_value(self, fixed_key, packed, default=None):
        if packed is None:
            return default

        value, refresh_time, refreshed = packed
        if (time.time() > refresh_time) and not refreshed:
            # Store the stale value while the cache revalidates for another
            # 30 seconds.
            real_timeout, packed = self.pack_values(value, 30, True)
            self._cache.set(fixed_key, packed, real_timeout)
            return None

        if isinstance(value, basestring):
            return smart_unicode(value)
        return value

Add calls to these methods from the base methods and your done.

    def add(self, key, value, timeout=0):
        fixed_key = self.fix_key(key)
        real_timeout, packed = self.pack_values(value, timeout)
        return self._cache.add(fixed_key, packed, real_timeout)

    def get(self, key, default=None):
        fixed_key = self.fix_key(key)
        packed = self._cache.get(fixed_key)
        return self.examine_value(fixed_key, packed, default)

    def set(self, key, value, timeout=0):
        fixed_key = self.fix_key(key)
        real_timeout, packed = self.pack_values(value, timeout)
        self._cache.set(fixed_key, packed, real_timeout)

    def delete(self, key):
        fixed_key = self.fix_key(key)
        self._cache.delete(fixed_key)

    def get_many(self, keys):
        fixed_keys = map(self.fix_key, keys)
        cache_dict = self._cache.get_multi(fixed_keys)
        final_dict = {}
        for fixed_key, packed_value in cache_dict.iteritems():
            key = keys[fixed_keys.index(fixed_key)]
            final_dict[key] = self.examine_value(fixed_key, packed_value)
        return final_dict