From 752659037686026e85c2da52c756d4bfec8c4d13 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Sat, 9 Aug 2008 15:07:45 +0000 Subject: [PATCH] Split CacheMiddleware up into two parts -- an update-cache and a fetch-from-cache middleware. This lets you run each half of the cache middleware at the correct time to avoid bad interactions between the cache middleware and other middleware that must modify the cache key (like the locale middleware). CacheMiddleware itself is still around for backwards-compatibility and as a hook point for the cache decorator, but the documentation has been updated to point people towards the two-part caching middleware. Refs #730. git-svn-id: http://code.djangoproject.com/svn/django/trunk@8260 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/middleware/cache.py | 188 ++++++++++++++++++++++++------------- docs/cache.txt | 43 ++++++--- 2 files changed, 152 insertions(+), 79 deletions(-) diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 23a907f8be..7301fdc005 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -1,76 +1,72 @@ +""" +Cache middleware. If enabled, each Django-powered page will be cached based on +URL. The cannonical way to enable cache middleware is to set +``UpdateCacheMiddleware`` as your first piece of middleware, and +``FetchFromCacheMiddleware`` as the last:: + + MIDDLEWARE_CLASSES = [ + 'django.middleware.cache.UpdateCacheMiddleware', + ... + 'django.middleware.cache.FetchFromCacheMiddleware' + ] + +This is counter-intuitive, but correct: ``UpdateCacheMiddleware`` needs to run +last during the response phase, which processes middleware bottom-up; +``FetchFromCacheMiddleware`` needs to run last during the request phase, which +processes middleware top-down. + +The single-class ``CacheMiddleware`` can be used for some simple sites. However, +if any other peice of middleware needs to affect the cache key, you'll need to +use the two-part UpdateCacheMiddleware and FetchFromCacheMiddleware. This'll +most often happen when you're using Django's LocaleMiddleware. + +More details about how the caching works: + +* Only parameter-less GET or HEAD-requests with status code 200 are cached. + +* The number of seconds each page is stored for is set by the "max-age" section + of the response's "Cache-Control" header, falling back to the + CACHE_MIDDLEWARE_SECONDS setting if the section was not found. + +* If CACHE_MIDDLEWARE_ANONYMOUS_ONLY is set to True, only anonymous requests + (i.e., those not made by a logged-in user) will be cached. This is a simple + and effective way of avoiding the caching of the Django admin (and any other + user-specific content). + +* This middleware expects that a HEAD request is answered with a response + exactly like the corresponding GET request. + +* When a hit occurs, a shallow copy of the original response object is returned + from process_request. + +* Pages will be cached based on the contents of the request headers listed in + the response's "Vary" header. + +* This middleware also sets ETag, Last-Modified, Expires and Cache-Control + headers on the response object. + +""" + from django.conf import settings from django.core.cache import cache from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age -class CacheMiddleware(object): +class UpdateCacheMiddleware(object): """ - Cache middleware. If this is enabled, each Django-powered page will be - cached (based on URLs). - - Only parameter-less GET or HEAD-requests with status code 200 are cached. - - The number of seconds each page is stored for is set by the - "max-age" section of the response's "Cache-Control" header, falling back to - the CACHE_MIDDLEWARE_SECONDS setting if the section was not found. - - If CACHE_MIDDLEWARE_ANONYMOUS_ONLY is set to True, only anonymous requests - (i.e., those not made by a logged-in user) will be cached. This is a - simple and effective way of avoiding the caching of the Django admin (and - any other user-specific content). - - This middleware expects that a HEAD request is answered with a response - exactly like the corresponding GET request. - - When a hit occurs, a shallow copy of the original response object is - returned from process_request. - - Pages will be cached based on the contents of the request headers - listed in the response's "Vary" header. This means that pages shouldn't - change their "Vary" header. - - This middleware also sets ETag, Last-Modified, Expires and Cache-Control - headers on the response object. + Response-phase cache middleware that updates the cache if the response is + cacheable. + + Must be used as part of the two-part update/fetch cache middleware. + UpdateCacheMiddleware must be the first piece of middleware in + MIDDLEWARE_CLASSES so that it'll get called last during the response phase. """ - def __init__(self, cache_timeout=None, key_prefix=None, cache_anonymous_only=None): - self.cache_timeout = cache_timeout - if cache_timeout is None: - self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS - self.key_prefix = key_prefix - if key_prefix is None: - self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX - if cache_anonymous_only is None: - self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) - else: - self.cache_anonymous_only = cache_anonymous_only - - def process_request(self, request): - "Checks whether the page is already cached and returns the cached version if available." - if self.cache_anonymous_only: - assert hasattr(request, 'user'), "The Django cache middleware with CACHE_MIDDLEWARE_ANONYMOUS_ONLY=True requires authentication middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.auth.middleware.AuthenticationMiddleware' before the CacheMiddleware." - - if not request.method in ('GET', 'HEAD') or request.GET: - request._cache_update_cache = False - return None # Don't bother checking the cache. - - if self.cache_anonymous_only and request.user.is_authenticated(): - request._cache_update_cache = False - return None # Don't cache requests from authenticated users. - - cache_key = get_cache_key(request, self.key_prefix) - if cache_key is None: - request._cache_update_cache = True - return None # No cache information available, need to rebuild. - - response = cache.get(cache_key, None) - if response is None: - request._cache_update_cache = True - return None # No cache information available, need to rebuild. - - request._cache_update_cache = False - return response + def __init__(self): + self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) def process_response(self, request, response): - "Sets the cache, if needed." + """Sets the cache, if needed.""" if not hasattr(request, '_cache_update_cache') or not request._cache_update_cache: # We don't need to update the cache, just return. return response @@ -95,3 +91,65 @@ class CacheMiddleware(object): cache_key = learn_cache_key(request, response, timeout, self.key_prefix) cache.set(cache_key, response, timeout) return response + +class FetchFromCacheMiddleware(object): + """ + Request-phase cache middleware that fetches a page from the cache. + + Must be used as part of the two-part update/fetch cache middleware. + FetchFromCacheMiddleware must be the last piece of middleware in + MIDDLEWARE_CLASSES so that it'll get called last during the request phase. + """ + def __init__(self): + self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) + + def process_request(self, request): + """ + Checks whether the page is already cached and returns the cached + version if available. + """ + if self.cache_anonymous_only: + assert hasattr(request, 'user'), "The Django cache middleware with CACHE_MIDDLEWARE_ANONYMOUS_ONLY=True requires authentication middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.auth.middleware.AuthenticationMiddleware' before the CacheMiddleware." + + if not request.method in ('GET', 'HEAD') or request.GET: + request._cache_update_cache = False + return None # Don't bother checking the cache. + + if self.cache_anonymous_only and request.user.is_authenticated(): + request._cache_update_cache = False + return None # Don't cache requests from authenticated users. + + cache_key = get_cache_key(request, self.key_prefix) + if cache_key is None: + request._cache_update_cache = True + return None # No cache information available, need to rebuild. + + response = cache.get(cache_key, None) + if response is None: + request._cache_update_cache = True + return None # No cache information available, need to rebuild. + + request._cache_update_cache = False + return response + +class CacheMiddleware(UpdateCacheMiddleware, FetchFromCacheMiddleware): + """ + Cache middleware that provides basic behavior for many simple sites. + + Also used as the hook point for the cache decorator, which is generated + using the decorator-from-middleware utility. + """ + def __init__(self, cache_timeout=None, key_prefix=None, cache_anonymous_only=None): + self.cache_timeout = cache_timeout + if cache_timeout is None: + self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS + self.key_prefix = key_prefix + if key_prefix is None: + self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX + if cache_anonymous_only is None: + self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) + else: + self.cache_anonymous_only = cache_anonymous_only + diff --git a/docs/cache.txt b/docs/cache.txt index d22dd91994..4ca9cbef72 100644 --- a/docs/cache.txt +++ b/docs/cache.txt @@ -231,17 +231,25 @@ arguments. The per-site cache ================== +**New in Django development version** (previous versions of Django only provided a single ``CacheMiddleware`` instead of the two pieces described below). + Once the cache is set up, the simplest way to use caching is to cache your -entire site. Just add ``'django.middleware.cache.CacheMiddleware'`` to your +entire site. You'll need to add +``'django.middleware.cache.UpdateCacheMiddleware'`` and +``'django.middleware.cache.FetchFromCacheMiddleware' to your ``MIDDLEWARE_CLASSES`` setting, as in this example:: MIDDLEWARE_CLASSES = ( - 'django.middleware.cache.CacheMiddleware', + 'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', + 'django.middleware.cache.FetchFromCacheMiddleware', ) -(The order of ``MIDDLEWARE_CLASSES`` matters. See `Order of MIDDLEWARE_CLASSES`_ -below.) +.. note:: + + No, that's not a typo: the "update" middleware must be first in the list, + and the "fetch" middleware must be last. The details are a bit obscure, but + see `Order of MIDDLEWARE_CLASSES`_ below if you'd like the full story. Then, add the following required settings to your Django settings file: @@ -258,10 +266,9 @@ parameters. Optionally, if the ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting is will be cached. This is a simple and effective way of disabling caching for any user-specific pages (include Django's admin interface). Note that if you use ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY``, you should make sure you've activated -``AuthenticationMiddleware`` and that ``AuthenticationMiddleware`` appears -before ``CacheMiddleware`` in your ``MIDDLEWARE_CLASSES``. +``AuthenticationMiddleware``. -Additionally, ``CacheMiddleware`` automatically sets a few headers in each +Additionally, the cache middleware automatically sets a few headers in each ``HttpResponse``: * Sets the ``Last-Modified`` header to the current date/time when a fresh @@ -627,16 +634,24 @@ apps' performance: Order of MIDDLEWARE_CLASSES =========================== -If you use ``CacheMiddleware``, it's important to put it in the right place -within the ``MIDDLEWARE_CLASSES`` setting, because the cache middleware needs -to know which headers by which to vary the cache storage. Middleware always -adds something to the ``Vary`` response header when it can. +If you use caching middlewaare, it's important to put each half in the right +place within the ``MIDDLEWARE_CLASSES`` setting. That's because the cache +middleware needs to know which headers by which to vary the cache storage. +Middleware always adds something to the ``Vary`` response header when it can. -Put the ``CacheMiddleware`` *before* any other middleware that might add -something to the ``Vary`` header (response middleware is applied in reverse -order). The following middleware modules do so: +``UpdateCacheMiddleware`` runs during the response phase, where middleware is +run in reverse order, so an item at the top of the list runs *last* during the +response phase. Thus, you need to make sure that ``UpdateCacheMiddleware`` +appears *before* any other middleware that might add something to the ``Vary`` +header. The following middleware modules do so: * ``SessionMiddleware`` adds ``Cookie`` * ``GZipMiddleware`` adds ``Accept-Encoding`` * ``LocaleMiddleware`` adds ``Accept-Language`` + +``FetchFromCacheMiddleware``, on the other hand, runs during the request phase, +where middleware is applied first-to-last, so an item at the top of the list +runs *first* during the request phase. The ``FetchFromCacheMiddleware`` also +needs to run after other middleware updates the ``Vary`` header, so +``FetchFromCacheMiddleware`` must be *after* any item that does so.