stonean

Dynamic Content and Action Caching

I recently deployed a Rails application that, for various reasons, required caching dynamic content. All dynamic content is represented by a core “Content” model that contains a url attribute. In order for my app to show these pages I have the following as my last route:

  map.dynamic ':p1',
  :controller => "contents",
  :action => "show",
  :requirements => {:p1 => /[\w|-]*/}

As you can see, the contents/show is my catchall action. If I can find a content url matching :p1, I show the content.

Another feature of the application is that you can restrict access to this content to one or many user groups via my Lockdown gem. At a basic level, Lockdown works by testing for access in a before_filter it adds to all controllers. To ensure I don’t expose protected content, I needed to use action caching to have my before filters execute before showing the page.

Action caching typically creates a cache file that corresponds to the action. However, in this case, the show action represents multiple urls. Here’s how you get around that:

class ContentsController < ApplicationController
  caches_action :show,
    :cache_path => proc{|c| c.send(:cached_page, c.params[:p1], c.params[:page]) }

  def show
   #logic to render multiple content types
  end

  private
  # This method will return my cache name
  def cached_page(url, page)
    page ||= "1"
    "#{url}_#{page}"
  end
end

If the url for the contents was “/about-us”, the cache would be “about-us”. If the content url was “/reunion-pictures?page=2″, the cache would be “reunion-pictures_2″. So forth and so on.

I was going setup a route that would turn “/reunion-pictures?page=2″ to “/reunion-pictures/2″, but for some reason, adding the route to do this broke the display of the page links in will_paginate. I’m not sure why and the url wasn’t that important to me. The value provided by will_paginate was more important to me than pretty routes in this instance. I may revisit this issue later…but I need to get back on point.

So…I’m caching now, cool. Now I just have to account for changing content, enter the sweepers:

class ImageSweeper < ActionController::Caching::Sweeper
  observe Image

  def after_create(post)
    expire_cache_for(post)
  end

  def after_update(post)
    expire_cache_for(post)
  end

  def after_destroy(post)
    expire_cache_for(post)
  end

  private
  def expire_cache_for(record)
    record.galleries.each do |gal|
    for i in 1..((gal.artworks.length / 10) + 1)
      expire_action("#{gal.url}_#{i}")
    end
  end
end

One type of dynamic content is a gallery that can contain multiple images. In order to make sure my galleries are kept up-to-date, I needed this sweeper to expire the caches of the galleries that contained the images I was creating/updating/deleting.

In order for this ImageSweeper to clean house, I needed to add the following to the ImagesController:

  cache_sweeper :image_sweeper,
    :only => [:create, :update, :destroy]

Now my galleries will stay current and I won’t be locked into showing cached galleries that represent old/incorrect information.

The cached pages are ridiculously faster…and yes, that’s a technical term similar to “ludicrous speed”.

Written by stonean

October 3, 2008 at 10:41 pm

Posted in RubyOnRails