Cache Invalidation

rails, fastly, cache invalidation

I recently experimented with serving this blogs content with Fastly. The page load speeds were tremendous. However, in the beginning I did feel some pain…

There are only two hard things in Computer Science: cache invalidation and naming things. – Phil Karlton

Out of the box Varnish supports flushing of URLs via the HTTP PURGE method. This works pretty well for simple one-to-one mappings of resources to URLs. However, complexity can arise when a URL depends on multiple resources within an application.

Luckily Fastly has done most of the heavy lifting for us and added Surrogate Keys to their service offering.

TL;DR we can return an HTTP header `Surrogate-Keys` that will allow us to build a many-to-many relationship between keys-and-pages within our site. When we purge a key; any page associated with that key will also be purged.

Creating Surrogate Keys

Using the codebase from this blog site as an example we’ll walk through the steps to set up Surrogate-Keys in a Ruby on Rails application.

Lets first add two convenience methods to our Article class so instances of it can generate their own keys.

# app/models/article.rb
class Article < ActiveRecord::Base
  def resource_key
    "#{collection_key}/#{id}"
  end

  def collection_key
    self.class.table_name
  end
end

Then we can use the `collection_key` and `resource_key` methods in the `ArticlesController` to generate the appropriate keys.

class ArticlesController < ApplicationController
  before_filter :set_cache_control_headers, only: [:index, :show]

  def index
    @articles = Article.published
    set_surrogate_header 'articles', @articles.map(&:resource_key)
  end

  def show
    @article = Article.find(params[:id])
    set_surrogate_header @article.resource_key
  end

  # ...

  private

  def set_surrogate_header(*keys)
    response.headers['Surrogate-Key'] = keys.join(' ')
  end
end

The `set_surrogate_header` method adds a new header to the Rails response and sets the value to the keys passed in (if we pass in multiple keys it will join them together into one space delimited string).

Lets verify we’re getting the expected response:

curl -X HEAD http://localhost:5000 -I
> HTTP/1.1 200 OK
> Surrogate-Key: articles articles/1 articles/2 articles/3

curl -X HEAD http://localhost:5000/articles/2 -I
> HTTP/1.1 200 OK
> Surrogate-Key: articles/2

Purging Surrogate Keys

When we modify an Article through the CMS we’ll want to purge the article “index page” and the “article detail” (as well as any other pages that might include the Article in question).

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def create
    @article = Article.new(article_params)

    if @article.save
      Fastly.purge(@article.collection_key)
    end

    respond_with @article
  end

  def update
    @article = Article.find(params[:id])

    if @article.update_attributes(article_params)
      Fastly.purge(@article.resource_key)
    end

    respond_with @article
  end

  private

  def article_params
    # ...
  end
end

Whenever a new Article is created the articles key will be purged (which will bust the cache for index page). Similarly, when an article is updated it will purge articles/:id which will bust the cache for the article page, and the index page.

If we were to add new pages to the site such as “Tags” or “Popular” we can use the same Surrogate Keys from Articles to programmatically purge the cache for those new pages too.

Purge in the Background

As purge traffic increases its worth exploring putting the Purge requests into a non-blocking background job.