Rails.cache: Memcached, development mode and offline cache invalidation

July 5, 2008 cache rails

This blog post was written in 2008. Information and links in this post may be outdated.

Rails.cache rocks, but it can be tricky to set it up for development mode. For my purposes I need to:

  • Keep config.cache_classes to false so that I don't have to restart my server while I develop
  • Cache all kinds of objects, not just strings
  • Be able to invalidate the cache easily from cron scripts or other offline processes
  • Test caching locally before deploying

The first thing I did was check out the excellent railscast and I read through the blog posts mentioned there. However, I couldn't quite figure out how to get things to work - I kept getting strange errors where all of the methods were being stripped from my classes, rails was complaining that my classes didn't exist or I was getting dreadful "singleton can't be dumped" errors. After a lot of googling and experimentation, here is what finally worked for me:

Environment files

I like to develop quickly, test caching on my local and then deploy. To accomplish this I have 3 environments, setup like so:

# config/environments/development.rb
config.action_controller.perform_caching = false
config.cache_classes = false
config.cache_store = :mem_cache_store, '127.0.0.1:11211', {:namespace => "dev"}

# config/environments/dev_with_caching.rb
config.action_controller.perform_caching  = true
config.cache_classes = true
config.cache_store = :mem_cache_store, '127.0.0.1:11211', {:namespace => "dev_with_caching"}

# config/environments/production.rb
config.action_controller.perform_caching  = true
config.cache_classes = true
config.cache_store = :mem_cache_store, '127.0.0.1:11211', {:namespace => "production"}

Here are a few interesting points:

You don't need to have memcached installed to develop locally

If you run your app locally without memcached installed, or without memcached running, you will see entries like this in your log

MemCacheError (No connection to server): No connection to server
Cache miss: Post.all ({:force=>false})

However, your app will work just fine. Rails will always execute the contents of the fetch blocks, and will return nil for any reads.

If memcached is running, you need to set cache_classes to true

To run memcached locally, you need to install memcached. I develop on a mac and manage packages with macports, so for me it was as easy as:

sudo port install memcached

Once memcached is installed, you can start it with

memcached -m 500 -l 127.0.0.1 -p 11211 -vv

which will print verbose logging to STDERR, or you can start it as a daemon like so:

memcached -m 500 -l 127.0.0.1 -p 11211 -d

Either of these will start a memcached process running on port 11211, and it will allocate 500MB RAM (most apps can get by with 128MB, or so I've heard).

Once this is running, though, you need to set config.cache_classes to true - otherwise you're app will blow up.

Marshal.dump is finicky

Rails.cache calls Marshal.dump on any object you try to put in the cache. Marshal won't work on everything though - and you may need to write your own serialization script. I've had problems with classes that have lots of module_eval statements that create methods dynamically and similar meta-programming techniques. If you start getting errors like "singleton can't be dumped", check to see if you have any meta-programming going on. I've also had issues with REXML objects.

If you do have an issue with a class that Rails won't cache, you can easily bypass the built-in serialization by writing your own dump and load methods. See the ruby docs for more info.

Use a separate environment to test locally

I have a new environment named dev_with_caching that I use to test caching locally. I set up my database.yml file so that it points to the development database, but performs caching and in all other respects mirrors the production environment. To test locally with that environment, I use:

script/server -e dev_with_caching -p 3001

Clearing the cache

I mostly use Rails.cache to cache data - and mostly for arrays of objects - like Category.all. As such, it's to keep all of this in the model, but cache invalidation can be trickly to manage. Here's a pattern I've started to use a lot:

class Category < ActiveRecord::Base

  after_save      :reset_cache
  after_destroy :reset_cache

  def reset_cache
    self.class.reset_cache
  end

  class << self

    def reset_cache
      cached_all(true)
    end

    def cached_all(force = false)
      Rails.cache.fetch("Category.all", :force => force) do
        Category.find(:all, :conditions => {:active=>true}, :order=>'position')
      end
    end
  end
end

Here's what's happening:

The first time you call Category.cached_all it looks for the "Category.all" item in the cache. If it's not there, it executes the contents of the block, and adds it to the cache. When you save or destroy a record the cache is invalidated.

If you want to force a refresh of the cache, just specify Category.cached_all(true) and it will be reloaded from the database. Once this is in place, it's easy to write cache invalidation scripts that both clear the cache and reload it at the same time.

I've done this by adding a class method that reloads the data, which is triggered by after_save and after_destroy callbacks. I'm sure there are a number of plugins that will do all that and more, but for my purposes this simple pattern works for me most of the time.

Clearing the cache with cron

Finally, if you want to clear the cache at specified intervals you can do so easily with rake and cron. First, create a rake task that calls the model's reset_cache method - since I normally have several classes with caching behavior I normally create a loop like so:

namespace :cache do
  namespace :reset do
    %w{Category Forum Post}.each do |klass|
      desc "Clear the #{klass} cache"
      task klass.underscore.gsub("/","_").pluralize => :environment do
        klass.constantize.reset_cache
      end
    end
  end
end

Now you can run rake cache:reset:categories and your Category.reset_cache method will be called. To make this work with cron, you'll need a slightly different syntax. The following command is suitable to execute from a cron script, or manually from the command line:

RAILS_ENV=production rake -f /var/www/apps/yourapp/current/Rakefile cache:reset:categories

It might take a little while to grok Rails.cache - but once you do your apps will be faster and you'll quickly become a wild caching fiend!

Tags