Redline Software Inc. - Winnipeg's Leader in Ruby on Rails Development

Conditional Cache Plugin Tutorial

Update

This plugin has now been DEPRECATED for Rails 2.2+ in favour of the built in :if and :cache_path options.

The following changes can be made to use :cache_path instead of the :tag option.

1. Change all instances of :tag to :cache_path
2. If the :tag referred to a sybmol such as :tag => :standard_tag, you simply need to change the method name that the symbol refers to as standard_tag_url (just add url to the end of the method name).
3. The host name no longer prefixes the cache fragment when :cache_path uses anything other than a hash, so make sure to begin your cache_path with the string generated from request.url (includes protocol) or request.host + request.request
uri (no protocol) if your cache fragment keys depend on the host name.

Updated for Rails 2.1

This is my first of many tutorials to come covering the plugins that we’ve developed.

This tutorial covers the conditional cache plugin, which is actually the first plugin released by Redline.

Why?

By default, the caches_action method allows you to specify one or more actions to apply caching to.

1
2
3
4
5
6
7
8
9
10
11
class TestController < ApplicationController
  caches_action :index, :new

  def index
    ...
  end

  def new
    ...
  end
end

In the above example, action caching is being applied to the index and new action. Beyond that we have no additional control over when these actions should be cached or how these actions should be cached. It’s either all or nothing.

So to fix this “all or nothing” problem, the conditional cache plugin adds additonal functionality to the caches_action method by adding 2 additional parameter options that can be passed to the method along with the existing list of actions.

:if option

Specifies a method, proc or string to call to determine if the caching should occur. The method, proc or string should return or evaluate to a true or false value.

This option behaves exactly like the :if option for validations, such as validates_format_of, validates_length_of, etc.

So an example using a method for the option would look like this…

1
2
3
4
5
6
7
8
9
10
11
12
class TestController < ApplicationController
  caches_action :index, :if => :allow_cache

  def index
    ...
  end

protected
  def allow_cache
    request.get?
  end
end

and using an inline Proc…

1
2
3
4
class TestController < ApplicationController
  caches_action :index, :if => Proc.new {|controller| controller.request.get?}
  ...
end

As you can see in the examples above, the “index” action will only cache during a GET request using the value specified for the :if option.

Generally I use the exact same test for my caching, so I create a method in the ApplicationController and then use it in my descendant controllers.

1
2
3
4
5
6
7
class ApplicationController < ActionController::Base
protected
  def standard_cache_test
    # only cache for a GET request and when the flash is empty
    request.get? && flash.empty?
  end
end

And then in descendant classes, I simply use the predefined method…

1
2
3
4
class TestController < ApplicationController
  caches_action :index, :if => :standard_cache_test
  ...
end

What the above test is saying is that caching should only occur for a GET request and that the flash is empty. I don’t want a page to be cached that contains flash data that may display on the page.

:tag option

Specifies a method, proc or string which adds an additional tag to the cache fragment path so that various states of caching can occur within the application. The method, proc or string should return or evaluate to a string. The tag (string) is appended to the end of the cache path on the server so the expire_fragment method should be used with a regex to safely expire actions that use the :tag option.

To better explain this, if you’re not aware, when an action is cached, a file is created in your apps tmp/cache directory by default. A path could look like tmp/cache/127.0.0.1:3000/test/index.cache

Using :tag, you can add your own text to this generated path for additional flexibility.

1
2
3
class TestController < ApplicationController
  caches_action :index, :tag => Proc.new { |controller| controller.user_type.to_s } # This will cache the page dependant on the type of user logged in. 
end

In this example, :tag returns a string version of a user_type. The user_type in this example would represent “admin”, or “moderator”, etc. If the generated index page differs for various user types, with say additional links, buttons, etc for an admin to manage various parts of the site, these different sections of the html should not be visible to a moderator or a normal user on the site.

So when an administrator loads the index page, the generated cache path would now look like tmp/cache/127.0.0.1:3000/test/indexadmin.cache

And when a moderator loads the index page, the generated cache path would look like tmp/cache/127.0.0.1:3000/test/indexmoderator.cache

If a normal user has no user_type associated with it and :tag returned an empty string, the cache path would look like it normally would if the :tag wasn’t used… tmp/cache/127.0.0.1:3000/test/index.cache

So as you can see from this example, the index action can be cached multiple ways depending on the type of user logged in.

Lets look at another example that uses a time zone tag associated with a user. This is helpful for pages that display time values where each user can set the time zone associated with their account, therefore seeing different times than a user with a different time zone setting.

1
2
3
4
5
6
7
8
9
10
11
class ApplicationController < ActionController::Base
protected
  def standard_cache_tag(options = {})
    logged_in? ? "/#{current_user.tz.utc_offset}" : ""
  end
end

class TestController < ApplicationController
  caches_action :index, :tag => :standard_cache_tag
  ...
end

As seen in the example for the :if option, I’ve created a predefined method to use in my descendant controller. What this method does is check if the user is logged in (using a method I’ve defined elsewhere called logged_in?) and adding the time zone associated with that user to the cache path, otherwise if the user is not logged in an empty string is returned.

So for a user not logged in, they would get the standard cache path of tmp/cache/127.0.0.1:3000/test/index.cache, but for a user logged in, the tag could generate a cache path of tmp/cache/127.0.0.1:3000/test/index/-21600.cache for one user and tmp/cache/127.0.0.1:3000/test/index/7200.cache for another. If you look at the tag created by the standard_cache_tag method, it adds a / delimiter to the tag, this just allows the cache path to create an additional directory.

So now the index action is cached with correct times for users viewing the page with different time zone settings.

Cleaning Up

For actions that are cached with an additional :tag, the usual method of using expire_action can still be used.

So using the example above…

1
2
3
class TestController < ApplicationController
  caches_action :index, :tag => Proc.new { |controller| controller.user_type.to_s } # This will cache the page dependant on the type of user logged in. 
end

We can expire the index action for an “admin” with expire_action…

1
expire_action :controller => 'test', :action => 'index', :tag => Proc.new {'admin'}

This method works, but it is somewhat static in that you can’t specify multiple strings for the :tag.

The preferred method of expiring cached data that uses a :tag is to use the expire_fragment method with a regular expression (regex).

We could then expire all index caches for the Test controller using…

1
expire_fragment %r{test/index.*}

This will match the path after the the main url, so both tmp/cache/127.0.0.1:3000/test/indexmoderator.cache and tmp/cache/127.0.0.1:3000/test/indexadmin.cache will match the above regex. Note that this will also match a path of tmp/cache/127.0.0.1:3000/test/index.cache which would result from caching the index action with no :tag.

If we want to expire the cache for the admin tag only, we could use…

1
expire_fragment %r{test/indexadmin.*}

This will only match the tmp/cache/127.0.0.1:3000/test/indexadmin.cache path. This example does the same thing as using expire_action in the example above, but for cases like these where you know what the :tag will be when expiring, using expire_action is preferred as it would perform faster over the expire_fragment approach.

Additional Usage

Of course the :if and :tag options can be used together…

1
2
3
4
class TestController < ApplicationController
  caches_action :index, :if => :standard_cache_test, :tag => :standard_cache_tag
  ...
end

And if you do use predefined methods, they can easily be combined with additional information…

1
2
3
4
5
6
7
8
9
10
11
12
class TestController < ApplicationController
  caches_action :index, :if =>:custom_cache_test, :tag => :custom_cache_tag

protected
  def custom_cache_test
    standard_cache_tag || test_some_other_condition
  end

  def custom_cache_tag
    standard_cache_tag + Date.now.day.to_s
  end
end

Summary

Hopefully this tutorial provides a thorough explanation to the additional benefits and flexibility that can be achieved using this plugin. If you use the plugin and find it handy, please leave comments here or visit the plugin page and leave a comment or a good rating. :)

Update for Rails 2.1

Removed the :if option since Rails now provides this functionality.

Comments