sábado, 24 de janeiro de 2015

Model caching in Rails

Recently I’ve been seeing some posts about using Redis as Rails model cache, so I decided that it was a good time to start writing about programming techniques and so contribute to the community. Ever since I saw the Railscasts Episode #115, I’ve been using Rails cache in my code to store models and reduce db requests. However it is equally simple to cache expensive calculations as well.
The setup is straight and is even in the config/environments/production.rb template. Just choose the cache store more suited to your needs.
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
There are some stores available out of the box: :memory_store, :file_store, and even :null_store, if you don’t want caching at all in some environment like test. This suggested store, mem_cache_store, will use Memcached as backend and will ask you to install the Dalli gem. Personally, I prefer the :redis_store (gem ‘redis-rails’), which uses Redis as backend, since my apps usually have Resque or Sideqik.
To make things easier, the ActiveSupport::Cache works like an interface, so you can change stores without change your app, or simply create your own :riak_store. The basic commands are read, write, fetch and delete. However you’ll find increment and decrement among others.
Rails.cache.read('fib_30') #=> nil
Rails.cache.write('fib_30', fibonacci(30)) #=> true
Rails.cache.read('fib_30') #=> 832040
Rails.cache.delete('fib_30') #=> true
Rails.cache.read('fib_30') #=> nil
Rails.cache.fetch('fib_41'){fibonacci(41)} #=> 165580141
The best way transparently read and write from the cache is to use the method fetch, passing a block with the code. This way when there is a cache miss, the block is executed and the result is cached and returned. An excellent source on fetch is Avdi Grimm’s Ruby Tapas episode 004 or Confident Ruby book.
In fact Ryan Bates implements the method cached_find with fetch as a cache for the ActiveRecord find. A simple, elegant one line solution:
def self.cached_find(id)
Rails.cache.fetch([name, id]) { find(id) }
end
def flush_cache
Rails.cache.delete([self.class.name, id])
end
It happens that ActiveRecord find method not only accepts a single id as argument, but also an array of ids, returning equally an array of records. Coherently, if we pass an array with a single id, we get back an array with a single record. However, the way the cache stores builds the cache key, the brackets simply disappear, creating a nasty bug in this solution.
User.cached_find( 1 )
# will result in
# key_cache => 'User/1'
# cache_value => #<User id:1>
And
User.cached_find( [1] ) # will result in
# key_cache => 'User/1'
# cache_value => [ #<User id:1> ]
This lead to a single key ‘User/1' with two distinct results, a record and an array with the record. As I experienced, such thing can pass tests and easily crash your production environment. Fortunately, this is easy to solve, just force the brackets into the key.
def self.cached_find(id)
Rails.cache.find([name, id.to_s]) { find(id) }
end
So now we have
User.cached_find(1) # ’User/1’ => #<User id:1>
User.cached_find([1]) # ’User/[1]’ => [ #<User id:1> ]
That’s all for now. In upcoming articles I’ll address matters like cache expiration/invalidation, cache bloat and how to write all these caching code and still comply with Sandy Metz rules.