Thursday, July 21, 2011

Caching Active Record Models in Rails 3 with Memcached, Dalli, and Cache-fu

Many of the common model caching solutions used for Rails 2 have become incompatible with the advent of Rails 3. Previously, developers could use gems like memcache-client, cache-fu, and cache-money to manage caching of their active record objects. Unfortunately, memcache-client and cache-money do not yet support Rails 3, making it unclear what the best caching solution is. But fear not! There is a painless way to cache your models in Rails 3.

Although memcache-client has been abandoned, the author of the gem has written a near drop-in replacement memcached api; dalli. Dalli offers the same api as memcache-client with the benefit of a leaner, faster codebase, written in pure Ruby. For simple model caching, a Rails 3 fork of cache-fu offers a simple acts_as_cached method that automatically caches your active record objects.

To install dalli and cache-fu in your Rails 3 app, simply add the following to your Gemfile and run bundle install.
# Gemfile
gem 'dalli'
gem 'cache_fu', :git => 'https://github.com/kreetitech/cache_fu.git'

To setup Rails to use the Dalli client for production, add the following to config/environments/production.rb:
# config/environments/production.rb
# Enable dalli (memcached) caching
config.cache_store = :dalli_store
config.action_controller.perform_caching = true
If you want to cache in any of your other environments, just add the same snippet to your environment of choice.

Dalli can also be used as rack middleware for session caching. To setup session caching with dalli, edit your config/initializers/session_store.rb to contain the following:
# config/initializers/session_store.rb
require 'action_dispatch/middleware/session/dalli_store'

Rails.application.config.session_store :dalli_store, 
  :memcache_server => '127.0.0.1:11211', 
  :namespace => 'sessions', 
  :key => '_yourappname_session', 
  :expire_after => 30.minutes

Now, whenever you use Rails' built in caching or session storage, dalli will automatically interface with memcached.

Cache-fu requires a little bit of setup too, but there's a rake task to help with configuration:
rake memcached:cache_fu_install
This will write a config/memcached.yml file for configuring model caching. Most of the default settings should be fine, but at the very least, you will have to set the default client to Dalli::client and update the production cache servers. Here's what I have:
# config/memcached.yml
defaults:
  client: Dalli::Client
  ttl: 1800
  readonly: false
  urlencode: false
  c_threshold: 10000
  compression: true
  namespace: yourappname
  benchmarking: false
  disabled: false
  debug: false

development:
  servers: 127.0.0.1:11211
  # Developers who have memcached installed on their local system should comment the following line
  # disabled: true

test: 
  disabled: true

staging:
  servers:
    - 127.0.0.1:11211

production:
  servers:
    - cache00.mysite.org:11211 
    - cache01.mysite.org:11211
    - cache02.mysite.org:11211

With that, cache-fu should be configured and ready to rock.

Now, it's time to add caching to your models. As an example, let's say I have a blog with a model called posts with attributes title:string, body:text, public:boolean. If I want to cache these posts, I just add the following to my model:
# app/models/post.rb
class Post < ActiveRecord::Base
  acts_as_cached
end

This gives your model some special helper methods for caching objects. A good way to use cache-fu is through the use of custom scopes. Basically, you define a scope in the model and then use the "cached" class method to access it via caching in the controller:

# app/model/post.rb
def self.recent
  where('created_at > ?', 1.week.ago).all
end

# app/controllers/posts_controller.rb
def recent_posts
  @posts = Post.cached(:recent)
end

Now, start you memcached server, fire up Rails, and take a look at your console output. If you access an action that uses "cached", you can now see memcached stats in the logger. Notice that the query in the self.recent method contains an all at the end. This is crucial in Rails 3, because otherwise just the relation will be cached and not the returned objects. I've barely scraped the surface of what cache-fu can do, but if you want a more in-depth analysis you can check out this blog post. If I left out any important details or you're having trouble, feel free to leave me a question in the comments.