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:

  • File store (ActiveSupport::Cache::FileStore) - uses individual files to store cached values.
  • Memory store (ActiveSupport::Cache::MemoryStore) - uses a simple hash to store values.
  • DRb store (ActiveSupport::Cache::DRbStore) - works just like the MemoryStore but uses a DRb server to store values.
  • Memcached store (ActiveSupport::Cache::MemCacheStore) - uses memcache and memcache-client (version 1.5.0 is bundled with Rails so there’s no need to install the Gem separately) to store values in a shared memory cache.
  • Compressed memcached store (ActiveSupport::Cache::CompressedMemCacheStore) - works just like the regular MemCacheStore but uses GZip to decompress/compress on read/write.

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! As Peter points out in the comments, 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:

  • Adds a cache_key method to ActiveRecord::Base that generates a key using the class name, record ID and updated_at timestamp (if available).
  • Provides the ActiveSupport::Cache.expand_cache_key method that builds a key using an optional namespace passed as a parameter to the method, the RAILS_CACHE_ID or RAILS_APP_VERSION environment variables (if they are set) and the result of a call to the cache_key or to_param method of the object being cached (if it responds to either of them) or in the case of an Array the expand_cache_key method is called for each element of the array and the resulting keys joined to form a single (potentially very long!) key.

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:

  • :namespace - specifies a string that will automatically be prepended to keys when accessing the memcached store.
  • :readonly - a boolean value that when set to true will make the store read-only, with an error raised on any attempt to write.
  • :multithread - a boolean value that adds thread safety to read/write operations - it is unlikely you’ll need to use this option as the Rails threadsafe! method offers the same functionality.

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!