Skip to content

Performance and caching

Important: All this needs review. Do it better and we can scale up many times and / or save on monthly bills

Table of contents (auto-built):

Our problem here is the original RestFUL-URL design, whereby the same URLs are used by the public and by logged in users, making them hard to cache.

Caching static stuff with Amazon Cloudfront

In theory our views should set headers telling Cloudfront which resources it can cache, and which should be served every time. Currently we cache for a short time only.

In practice, we aren't sure it's happening; making it work right probably needs whitenoise and then some careful checks from outside.

This would involved compiling/compressing staatic Javascript and CSS and images into a file with a unique hash, deploying it to a url under /static/, and telling AWS Cloudfront to cache anything under /static/ forever - since any rebuild of the servers with even one byte change in static assets would make a fresh file.

Caching in Redis within Django

We have Redis (actually AWS Elasticache) configured on the servers. This is like a database that never serves anything to disk. It is cheap to put stuff there and retrieve it. This is Django's cache backend and also used by app code

We'd often like the public to see slightly old pages, so high interest at a major event doesn't kill our servers. So we have a cache_anon decorator which says "If I served this to anyone else in the last 5 minutes, serve up the same copy. You will see a @cache or @cache_anon decorator above view functions to tell you this happens. These are functions which check if something is in the cache.

Generally, we're caching stuff for up to 5 minutes, so a lot of public interest doesn't swamp us. A smarter approach would say "cache stuff happening now for one minute, stuff a bit older or not-yet-happened for 5, and anything that's not on today for hours"

Exempting logged in users

However, we want logged in users to be handled as usual by Django. We achieve this by writing some middleware, which sets a Cookie with the name nocache on requests for logged in users if not set. You can see above that there is a line uwsgi_cache_bypass $cookie_nocache which means do not look for a page in the cache, if there is a cookie named nocache present on the request.

The middleware is defined as follows, in project/comp/middleware.py:

class AuthenticatedNoCacheMiddleware(MiddlewareMixin):

    def process_response(self, request, response):

        if (((not hasattr(request, 'host')
            or request.host.name in ['federation', 'data'])
                and langPat.match(request.path))
                and '/accounts/' not in request.path):

            usr = getattr(request, 'user', False)
            if usr and usr.is_authenticated:
                if 'nocache' not in request.COOKIES:
                #    response.delete_cookie('nocache')
                # else:
                    response.set_cookie('nocache', '1', 60 * 10)
            else:
                if settings.SESSION_COOKIE_NAME in response.cookies:
                    del response.cookies[settings.SESSION_COOKIE_NAME]
                if 'nocache' in request.COOKIES:
                    response.delete_cookie('nocache')
                # if 'Vary' in response:
                #   del response['Vary']

        return response

This basically says

  1. Never cache accounts URLs like login, reset password etc, which need various cookies
  2. If they are logged in, add the nocache cookie so Nginx knows not to cache the page
  3. If they are NOT logged in, make sure the anonymous session cookie their browser might set is removed from our response, so that Nginx treats disparate users as being the same anonymous user

Caching data structures within Django

As well as caching whole pages, we sometimes cache structures. For example, for the page for editing a round with several heats, on first call we query a ton of Mongo structures and put them in a dictionary. Then put it in the cache under a certain key, and the next time, just grab that.

The strategy here is that cache entries get prefixed by something like a URL, with the competition, event and heat. Then, when a unit (heat) is saved, we quickly delete any cache entries starting with that prefix. So effectively, the json structures are cached "until results change". These are triggered by signals - look for post_save