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:1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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:
1 2 3 4 5 6 7 8 9 10 |
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:categoriesand 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!