Caching Rails Objects in Memcache and the cache_lookup gem

January 31, 2013 Link to post  Permalink

Ruby on Rails has the ability to easily save the results of expensive operations to memcache for later use without the overhead of that expensive operation. Some things are easier to cache than others, and today I’m going to talk about one of the easiest things to cache in a Rails application - a single object, typically for a show action.

The Rails Cache Store

The Rails documentation lists all the different cache stores that can be used in your application. For any non-trivial application, one that will run on more than a single web server, then you’ll need to use something like memcache to store your data and share it between servers.

The current best way of accessing a memcache store is the Dalli gem. This will be the default store in Rails 4

Here’s my dalli configuration from production.rb for this blog on Heroku

config.cache_store = :dalli_store, ENV["MEMCACHIER_SERVERS"],  
  {
    :username => ENV["MEMCACHIER_USERNAME"],  
    :password => ENV["MEMCACHIER_PASSWORD"],  
    :namespace => 'craz8:v1' 
 }

Reading and Writing a single object

Here’s the code we’re going to improve:

def show
  @post = Post.find(params[:id])
  fresh_when …..
end  

First, I’m going to put all our cache magic in the Model to keep it out of the Controller code

# controller
def show
  @post = Post.lookup_by_id(params[:id])
  fresh_when …..
end  

# Post model
class Post < ActiveRecord::Base

  def self.lookup_by_id(id)
    Rails.cache.fetch "post_by_id:#{id}" do
      where(:id => id).first
    end
  end

end

The Rails.cache.fetch method performs a lookup for the key you supply, and if there is no data there, then the code inside the block is executed, and the value that is returned from the block is stored in the cache. We have to provide two things - a cache key that uniquely identifies this data, and the block that returns that data.

Notice that, inside the block, we have to call first method to retrieve the object. ActiveRecord tries hard to defer the actual database query as long as possible, but in this case we need to run the query to get the data from the server. If we didn’t try to access the data, the object we’d be trying to save to the cache would be an instance of ActiveRecord::Relation. Luckily, objects of this type can’t be marshaled, and the save to the cache will fail with an exception.

Caveat - Check for Nil returns

This code isn’t a direct replacement for Post.find, as the find method will raise an ActiveRecord::RecordNotFound exception instead of returning nil. The lookup_by_id method above will return nil, so you will need to check for this in your controller, or other code.

What happens when the data changes?

Cache expiry is one of the hard problems in computer science. We want to make sure that the cache is expired any time the object is changes, and also if the object is deleted, the cache should be cleared. For the single object case, the expiry is quite easy to achieve with the help of ActiveRecord callbacks after_commit and after_destroy

class Post < ActiveRecord::Base
  after_commit   :clear_id_cache
  after_destroy  :clear_id_cache

protected
  def clear_id_cache
     Rails.cache.delete "post_by_id:#{id}"
  end
end

cache_lookup gem

I’ve packaged all of this into a new Gem - cache_lookup that allows you to access data by different attributes. The documentation shows an example that uses both id and slug to access the Post model.

Please take a look and let me know if you find a way to use this in your own code.

The next issue of my Faster Rails Newsletter is due to be released at the end of February. Use the sign up form to the right now to ensure you get it when it is available.