Crosstips.org

My fun Crossword solver project. Crosstips.org & Krysstips.se

Kung Fu

Fujian White Crane Kung Fu

Fry-IT

Fry-IT is the company I work for

Photos

Photoalbum, both old and new.

Zope

What I have and am doing with Zope

Receptsamlingen

In Swedish only. About my "Collection of Recipes" website.

Contact me

My contact details and how to contact me.

 

KungFuPeople.com
Do you train Kung Fu?
Or know someone who does?
Then check out KungFuPeople.com


Mobile version of this page Mobile version of this page


 

Custom CacheMiddleware that tells Javascript a page is cached in Django


cachemiddleware, cached, memcache, cache-control, expires, flicker, decorator, custom_cache_page, decorators , cache_page, decorator_from_middleware, nginx, cache_control

24th of August 2009

Here I'm going to explain a solution I had to make for a site I recently launched. Basically, I wanted to cache the whole page in memcache and set the appropriate Expires and Cache-Control headers so that my view was only rendered once an hour and parts of the page needs to be unique (i.e. "Hi, logged in as xxxx")

The advantages is great: The page loads fast, content is stored in memcache every hour, page still appears to be dynamic.

The disadvantages are not so great: the AJAX loads fast but causes a flicker

Basically, I wrote a custom decorator called custom_cache_page(<delay in seconds>) that works like the normal cache_page(<delay in seconds>) decorator available in stock Django. However, my decorator inserts a piece of HTML into the rendered HTML (before it's stored in memcache) that I later use to update certain elements of the page with AJAX instead of server side. Enough talking, let's look at the code!:

 from django.utils.decorators import decorator_from_middleware
 from django.middleware.cache import CacheMiddleware
 class CustomCacheMiddleware(CacheMiddleware):
    def __init__(self, cache_delay=0, *args, **kwargs):
        super(CustomCacheMiddleware, self).__init__(*args, **kwargs)
        self.cache_delay = cache_delay

    def process_response(self, request, response):
        if self.cache_delay:
            extra_js = '<script type="text/javascript">var '\
                       'CACHE_CONTROL=%s;</script>' %\
                        self.cache_delay
            response.content = response.content.replace(u'</body>',
                                              u'%s\n</body>' % extra_js)

        return super(CustomCacheMiddleware, self
                    ).process_response(request, response)

 custom_cache_page = decorator_from_middleware(CustomCacheMiddleware)

 if settings.DEBUG:
    def custom_cache_page(delay):
        def rendered(view):
            def inner(request, *args, **kwargs):
                return view(request, *args, **kwargs)
            return inner
        return rendered

 @custom_cache_page(60 * 60 * 1) # 1 hours
 def my_expensive_but_cacheable_view(request):
    # only run max. once every hour
    complex_and_cpu_intensive_calculation()
    ...

What you now get is that in every page that is server-side cached in memcache (and set Expires and Cache-Control headers) get a little piece of Javascript code inserted into the rendered HTML. Now, all I need to do is to write some jQuery code that loads the navigation menu dynamically but instigate it from a AJAX request. Your mileage here might vary (put this in your base.html or whatever you call it):

 $(function() {
   if (typeof CACHE_CONTROL != "undefined" && CACHE_CONTROL) {
      // the page is cached, need to use AJAX to load what should be dynamic
      $('#nav').load('/_nav.html');
   }
 });

I usually prefix all my views with an underscore when they only return a limited chunk of HTML rather than a whole HTML document.

Discussion

98% of my pages are deliberately not cached because they either aren't expensive to render or really can't be cached because of logged in users or whatnot.

This solution has the added benefit of being totally contained in Django, so it doesn't require any work on the nginx/apache/lighttpd front end but if you really want some speed (e.g. getting nginx talking directly to memcache) you can still use the above solution to get what you want.

My little decorator can be improved admittedly. You might perhaps add a dynamic check if the page should not be cache for example. Or perhaps some other available trick for invalidating the cache if you're really need to. Or perhaps some other tricks can be made to automate the Javascript AJAX loading.



Comment

Tiago S. - 29th August 2009  [«« Reply to this]
Thanks for the tip. I liked they way you handled it.

I've seen an approach like this before(ie, loading a small snippet of uncacheable data via js) and your approach is a lot cleaner.
 
Name:
Email:
hide my email address.

Your email address will be encoded to prevent email-extraction spiders from reading it so you won't get spammed if you decide to show your email address.