We build Web & Mobile Applications.

< All Articles

Rails 2.1: now with better integrated caching

Rails 2.1 has just been out a week and so far something that seems to have passed most people by is that it now includes much better caching capabilities, including built-in support for memcached.

Last week I reached the point with an application where I needed to cache some models in memory to get a performance boost and decided to check out the current status of plugins like cache_fu and CachedModel to make sure they’d work with Rails 2.1. It was completely by accident that I stumbled across this innocent looking commit by DHH from start of this year and realised that Rails already had everything I needed!

Making sense of my notes

Unfortunately the documentation for this new caching support seems quite scarce and unusually Google couldn’t help much either as there hasn’t been much blogging about it yet. So, not for the first time, I spent a bit of time digging into the source code to find out how it all fits together. What follows is a write-up of my hastily scribbled notes.

The basics

The code lives in the ActiveSupport::Cache module and it defines a base Store class which provides the basic interface for a caching store with methods like: read, write, delete and exist? (why oh why isn’t it exists? like ActiveRecord?). There is also a ThreadSafety module that wraps read/write operations in a Mutex#synchronize block. Enabling thread safety on a cache store is a simple matter of calling the threadsafe! method.

Rails includes five cache stores:

Rails automatically creates a global cache during initialisation and you can access this in your code either using the RAILS_CACHE global variable or the (in my opinion nicer looking) Rails.cache. Here are a few simple examples:

# Write a string to the cache
Rails.cache.write('test_key', 'test_value')
=> true

# Read it back in
Rails.cache.read('test_key')
=> 'test_value'

# Delete the item
Rails.cache.delete('test_key')
=> true

By default if the tmp/cache directory exists in your application root then Rails will use a FileStore otherwise it’ll use a MemoryStore as the global cache. Either of these options should be fine for development and testing, but for production you’ll almost certainly want to consider using the MemCacheStore. One of the big advantages of this new code is that for the most part it allows you to switch cache store without having to change any of your code.

Important! This behaviour is not currently implemented (the code is there but not used) - I’ve submitted a bug report

Changing the global cache store

Rails provides the config.cache_store option in environment.rb to allow you to change the cache store. It accepts either a class name or symbol for the required store and an array of options used when initialising the store. Some examples:

# Use the memory store - this store has no options
config.cache_store = :memory_store

# Use the file store with a custom storage path (if the directory doesn't already exist it will be created)
config.cache_store = :file_store, '/my_cache_path'

# Use the memcached store with default options (localhost, TCP port 11211)
config.cache_store = :mem_cache_store

# Use the memcached store with an options hash
config.cache_store = :mem_cache_store, 'localhost', '192.168.1.1:1001', { :namespace => 'test' }

Behind the scenes this makes use of the ActiveSupport::Cache.lookup_store method, which you can also call directly to create a cache of your own:

# Create a separate memory cache in addition to the global Rails cache
my_cache = ActiveSupport::Cache.lookup_store(:memory_cache)

Data keys

All of these cache stores provide what is essentially a Hash that can be read from and written to using a key. Just like with a regular Hash object, this key needs to be unique and not just within the scope of your application but also, in the case of shared caches like memcached, across all applications that are using the cache. To make cache key generation a little easier Rails does two things:

MemCacheStore specifics

The MemCacheStore accepts an array of server addresses in the form hostname[:port][:weight]. For example, if you have two servers on different ports you could specify them like so:

config.cache_store = :mem_cache_store, '192.168.1.1:11000', '192.168.1.2:11001'

It also accepts a hash of additional options:

The read and write methods of the MemCacheStore accept an options hash too. When reading you can specify :raw => true to prevent the object being marshaled (by default this is false which means the raw value in the cache is passed to Marshal.load before being returned to you.

When writing to the cache it is also possible to use the :raw option which when false (the default) means the value is passed to Marshal.dump before being stored in the cache. The write method also accepts an :unless_exist flag which determines whether the memcached add (when true) or set (when false) method is used to store the item in the cache and an :expires_in option that specifies the time-to-live for the cached item in seconds. A few examples:

# Write a raw value to the cache
Rails.cache.write('test_key', 1, :raw => true)

# Read a raw value from the cache
Rails.cache.read('test_key', :raw => true)

# Write a value using the add method and setting expiry to 15 minutes
Rails.cache.write('test_key', 'test_value', :unless_exist => true, :expires_in => 15.minutes)

Increment and decrement

As caches are often used to store counters all of the cache stores provide increment and decrement methods. For example:

# Store an initial counter value in the cache
Rails.cache.write('number_of_cakes_eaten', 1)

# Increment the counter by 5
Rails.cache.increment('number_of_cakes_eaten', 5)

This encapsulates the process of retrieving a value, incrementing or decrementing it and then storing the modified value back in the cache. In addition, the MemCacheStore takes advantage of memcached atomic increment/decrement functionality to perform this action in a single call to the cache.

Custom caches

You now know enough about Rails caching to make good use of it in your own code. If you want to take things a step further and implement your own custom cache store you might want to take a look at Ryan Daigle’s example - his blog entry also demonstrates how to configure ActionController caching in Rails 2.1.

And finally…running memcached on Windows (groan!)

As a long suffering Windows developer you may well have read all of this, gotten quite excited about using memcached, and then realised that you’ve got to get it running on Windows. It’s normally at this point that I end up getting annoyed and resolve to buy a Mac, but I’m pleased to say on this occasion I was pleasantly surprised. I simply did the following:

  1. Downloaded this Win32 port of memcached by Kenneth Dalgleish.
  2. Extracted the files to C:\Program Files\memcached
  3. Opened a command prompt in the folder and installed memcached as a service using memcached -d install
  4. Started the service using net start "memcached Server"

And, shockingly, it just worked!

Updated on 07 February 2019
First published by Rob Anderton on 09 June 2008
© Rob Anderton 2019
"Rails 2.1: now with better integrated caching" by Rob Anderton at TheWebFellas is licensed under a Creative Commons Attribution 4.0 International License.