We build Web & Mobile Applications.
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!
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 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! This behaviour is not currently implemented (the code is there but not used) - I’ve submitted a bug report
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)
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.
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)
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.
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.
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:
C:\Program Files\memcached
memcached -d install
net start "memcached Server"
And, shockingly, it just worked!