Expiring Cached Pages

Problem

You're caching pages of your application using the Rails page-caching mechanism, and you need a system for removing cached pages when the data that was used to create those pages changes.

Solution

To remove pages that have been cached when content is updated, you can call expire_page in the update action of your controller; for example:

def update
 @recipe = Recipe.find(params[:id])
 if @recipe.update_attributes(params[:recipe])
 flash[:notice] = 'Recipe was successfully updated.'
 expire_page :controller => "recipes", :action => %W( show new ), 
 :id => params[:id]
 redirect_to :action => 'show', :id => @recipe 
 else 
 render :action => 'edit'
 end end

Caching expiration often gets more complicated when you have pages that share content form related models, such as an article page that displays a list of comments. In this case, when you update a comment, you need to make sure that you expire the cache of the comment you're updating as well as its parent article. Adding another expire_page call takes care of this:

def update
 @comment = Comment.find(params[:id])
 if @comment.update_attributes(params[:comment])
 flash[:notice] = 'Comment was successfully updated.'
 expire_page :controller => "comments", :action => "show", 
 :id => @comment.id
 expire_page :controller => "articles", :action => "show", 
 :id => @comment.article_id
 redirect_to :action => 'show', :id => @comment
 else
 render :action => 'edit'
 end end

This example removes the cached page of the comment being updated as well as the related article page based on the article_id from the @comment object.

Discussion

Rails page caching usually starts out being a simple solution to performance problems but can quickly become a problem of its own when page cache expiration becomes more complex. The symptoms of caching complexities are usually pages that don't get expired when they should.

One approach to cache expiration complication is to delete all the files in a particular area of the cache when any of the cached data has changed or been deleted. Additionally, Rails provides a facility for organizing your cache expiration code called sweeper classes, which are sub classes of ActionController::Caching::Sweeper.

The following shows how to use a sweeper to remove all cached files in an application when either an article or comment is updated or deleted.

First, let's assume you've set your page cache directory to a directory beneath public:

config/environment.rb:

config.action_controller.page_cache_directory = \
 RAILS_ROOT+"/public/cache/"

To keep things organized, you can store your cache sweepers in app/cachers. To get Rails to include this directory in your environment, add the following to your configuration via environment.rb:

config/environment.rb:

Rails::Initializer.run do |config|
 # ...
 config.load_paths += %W( #{RAILS_ROOT}/app/cachers )
end

Then define a CacheSweeper class with the following:

class CacheSweeper < ActionController::Caching::Sweeper
 observe Article, Comment
 def after_save(record)
 self.class::sweep
 end
 def after_destroy(record)
 self.class::sweep
 end
 def self.sweep
 cache_dir = ActionController::Base.page_cache_directory
 unless cache_dir == RAILS_ROOT+"/public"
 FileUtils.rm_r(Dir.glob(cache_dir+"/*")) rescue Errno::ENOENT
 end
 end end

The CacheSweeper acts as an observer (observing changes to the Article and Comment classes) and also as a filter. The filtering behavior is set up by passing the name of the sweeper class and conditions about what actions it is to filter to the cache_sweeper method in your controller:

class ArticlesController < ApplicationController
 caches_page :show
 cache_sweeper :article_sweeper, :only => [ :edit, :destroy ]
 #...
end

Any time an article record is saved or deleted the following is called:

FileUtils.rm_r(Dir.glob(cache_dir+"/*")) rescue Errno::ENOENT

This action simply removes the entire contents of your cache directory. Whether you choose this method or a more granular cache expiration method depends on the specific performance requirements of your application.

See Also

  • memcached can be set up to automatically expire your cache; see