Changeset 108

Show
Ignore:
Timestamp:
11/07/09 18:23:31 (10 months ago)
Author:
max
Message:

Added new automated template fragment cache invalidation.

Location:
trunk/courant/core/caching
Files:
9 added
3 modified

Legend:

Unmodified
Added
Removed
  • trunk/courant/core/caching/__init__.py

    r1 r108  
     1from actions import * 
  • trunk/courant/core/caching/models.py

    r1 r108  
    1 # filler so courant.core.caching gets recognized as installable app 
     1from django.db import models 
     2from django.db.models.signals import post_save 
     3from django.core.cache import cache 
     4from django.contrib.contenttypes.models import ContentType 
     5from django.contrib.contenttypes import generic 
     6from django_extensions.db.fields import ModificationDateTimeField 
     7from django.conf import settings 
     8 
     9class CachedObject(models.Model): 
     10        cache_key = models.CharField(max_length=255) 
     11        url = models.CharField(max_length=255) 
     12        content_type = models.ForeignKey(ContentType) 
     13        object_id = models.PositiveIntegerField() 
     14        content_object = generic.GenericForeignKey('content_type', 'object_id') 
     15        modified_at = ModificationDateTimeField() 
     16 
     17def clear_obj_cache(obj): 
     18        try: 
     19                obj_caches = CachedObject.objects.filter(content_type=ContentType.objects.get_for_model(obj), 
     20                                                                                                 object_id=obj.pk) 
     21                urls = [] 
     22                for obj_cache in obj_caches: 
     23                        # mark object's cache as stale so it will be safely regenerated 
     24                        cache.delete("%s.stale" % obj_cache.cache_key) 
     25 
     26                        # mark URL as needing cache clearing 
     27                        urls.append(obj_cache.url) 
     28 
     29                # delete caches for all marked URLs 
     30                # since full-page caches don't use anti-dogpiling, we delete the caches themselves 
     31                for url in set(urls): 
     32                        key = "%s-%s" % (settings.CACHE_KEY_PREFIX, url) 
     33                        print "Deleting cache: %s" % key 
     34                        cache.delete(key) 
     35        except: 
     36                pass 
     37 
     38def clear_obj_cache_signal(sender, instance, **kwargs): 
     39        clear_obj_cache(instance) 
     40post_save.connect(clear_obj_cache_signal) 
  • trunk/courant/core/caching/templatetags/smart_cache.py

    r1 r108  
    33from django.core.cache import cache 
    44from django.utils.encoding import force_unicode 
     5from django.contrib.contenttypes.models import ContentType 
     6 
     7from courant.core.caching.models import CachedObject 
    58 
    69register = Library() 
     
    1316    STALE_CREATED = 2 
    1417 
    15     def __init__(self, nodelist, expire_time, fragment_name, vary_on): 
     18    def __init__(self, nodelist, expire_time, fragment_name, vary_on, cache_obj=None): 
    1619        self.nodelist = nodelist 
    1720        self.stale_time = expire_time 
     
    1922        self.fragment_name = fragment_name 
    2023        self.vary_on = vary_on 
     24        self.cache_obj = cache_obj 
    2125 
    2226    def render(self, context): 
     
    3135            value = None # force refresh 
    3236        if value is None: 
     37            context.push() 
     38            context['cache_key'] = cache_key 
    3339            value = self.nodelist.render(context) 
     40            context.pop() 
    3441            cache.set(cache_key, value, self.expire_time) 
    3542            cache.set(cache_key_stale, self.STALE_CREATED, self.stale_time) 
     43            if self.cache_obj: 
     44                obj = resolve_variable(self.cache_obj, context) 
     45                co, created = CachedObject.objects.get_or_create(url=context['request'].get_full_path(), 
     46                                                                 content_type=ContentType.objects.get_for_model(obj), 
     47                                                                 object_id=obj.pk, 
     48                                                                 cache_key=cache_key) 
     49                co.save() # update modified timestamp 
    3650        return value 
    3751 
     
    4357    effect. 
    4458 
    45     You can easily replace the default template cache, just change the load 
    46     statement from ``{% load cache %}`` to ``{% load cache_smart %}``. 
    47  
    4859    Usage:: 
    4960 
    50         {% load cache_smart %} 
    5161        {% cache [expire_time] [fragment_name] %} 
    5262            .. some expensive processing .. 
     
    5565    This tag also supports varying by a list of arguments:: 
    5666 
    57         {% load cache_smart %} 
    5867        {% cache [expire_time] [fragment_name] [var1] [var2] .. %} 
    5968            .. some expensive processing .. 
     
    6170 
    6271    Each unique set of arguments will result in a unique cache entry. 
     72     
     73    To enable automatic cache invalidation, a model object can be passed as 
     74    a final optional parameter. Whenever the object is saved, this template 
     75    fragment will be regenerated:: 
     76         
     77        {% cache [expire_time] [fragment_name] [var1] for [obj] %} 
     78            .. some expensive processing .. 
     79        {% endcache %} 
     80         
     81    See the 'cache_dup' template tag to enable automatic cache invalidation 
     82    based on more than a single object. 
    6383    """ 
    6484    nodelist = parser.parse(('endcache', )) 
     
    7494            u"First argument to '%r' must be an integer (got '%s')." % 
    7595            (tokens[0], tokens[1])) 
     96    if (tokens[-2] == 'for'): 
     97        return SmartCacheNode(nodelist, expire_time, tokens[2], tokens[3:-2], tokens[-1]) 
    7698    return SmartCacheNode(nodelist, expire_time, tokens[2], tokens[3:]) 
    7799 
    78100register.tag('cache', do_smart_cache) 
     101 
     102class CacheDependencyNode(Node): 
     103    def __init__(self, deps): 
     104        self.deps = deps 
     105 
     106    def render(self, context): 
     107        if 'cache_key' in context: 
     108            for dep in self.deps: 
     109                obj = resolve_variable(dep, context) 
     110                co, created = CachedObject.objects.get_or_create(url=context['request'].get_full_path(), 
     111                                                                 content_type=ContentType.objects.get_for_model(obj), 
     112                                                                 object_id=obj.pk, 
     113                                                                 cache_key=context['cache_key']) 
     114                co.save() # update modified timestamp 
     115        return '' 
     116 
     117def do_cache_dependency(parser, token): 
     118    """ 
     119    When used inside a {% cache %} template tag block, this template tag will 
     120    cause the entire cache block to be regenerated whenever any of the passed 
     121    parameters change:: 
     122         
     123        {% cache_dup [var1] %} 
     124        {% cache_dup [var1] [var2] %} 
     125         
     126    Example usage:: 
     127         
     128        {% cache .. for object_a %} 
     129            .. expensive processing .. 
     130            {% cache_dup object_b %} 
     131            .. more processing .. 
     132            {% cache_dup object_c object _d %} 
     133        {% endcache %} 
     134         
     135    In this example, when the model objects referred to by 'object_a',  
     136    'object_b', 'object_c', or 'object_d' are saved, this template fragment 
     137    will be automatically invalidated and regenerated.  
     138     
     139    There is no limit to the number of times 'cache_dup' can be called within 
     140    a single 'cache' block. 
     141    """ 
     142    bits = token.split_contents() 
     143    if len(bits) > 1: 
     144        return CacheDependencyNode(bits[1:]) 
     145    return '' 
     146register.tag('cache_dep', do_cache_dependency)