cache_method

It's like alias_method, but it's cache_method!

Lets you cache the results of calling methods given their arguments. Like memoization, but stored in Memcached, Redis, etc. so that the cached results can be shared between processes and hosts.

Real-world usage

Brighter Planet logo

We use cache_method for data science at Brighter Planet and in production at

Rationale

  • It should be easy to cache instance methods
  • It should be easy to cache methods that depend on object state
  • It should be easy to uncache a method without clearing the whole cache
  • It should be easy to do all that using a default in-process store, memcached, dalli (if you're on heroku), redis, etc. (all supported by this gem through the cache gem)

Example

require 'cache_method'
class Blog

  attr_reader :name, :url

  def initialize(name, url)
    @name = name
    @url = url
  end

  def entries(date)
    # ...
  end

  # cache that slow method!
  cache_method :entries

  def update(stuff)
    # ...
  end

  # automatically clear cache for #entries when #update is called...
  cache_method_clear_on :update, :entries

  # custom cache key - not always required!
  def as_cache_key
    { :name => name, :url => url }
  end
end

Then you can do

my_blog.entries(Date.today) => first time won't be cached
my_blog.entries(Date.today) => second time will come from cache

And clear them too

my_blog.cache_method_clear :entries

(which doesn't delete the rest of your cache)

Configuration (and supported cache clients)

By default, an in-process, non-shared cache is used.

You can set where the cache will be stored:

CacheMethod.config.storage = Memcached.new '127.0.0.1:11211'

or

CacheMethod.config.storage = Redis.new

or this might even work...

CacheMethod.config.storage = Rails.cache

See Config for the full list of supported caches.

Cache keys

Caching a method involves getting cache keys for

  1. the object where the method is defined - for example, my_blog.as_cache_key
  2. the arguments passed to the method - for example, Marshal.dump(Date.today), because Date#as_cache_key is not defined

Then the cache keys are SHA-1 hashed and combined for an overall key:

method signature + obj digest                                                                           + args digest
Blog#entries     + SHA1(Marshal.dump({:name="Seamus's blog",:url=>"http://numbers.brighterplanet.com"}) + SHA1(Marshal.dump(Date.today))

Technically the full cache key is

# when caching class methods
method signature + generation + args digest
# when caching instance methods
method signature + obj digest + generation + args digest

(see "Generational caching" below for an explanation of the generation part)

#as_cache_key

As above, you can define a custom cache key for an object:

class Blog
  # [...]
  def as_cache_key
    { :name => name, :url => url }
  end
  # [...]
end

If you don't define #as_cache_key, the default is to Marshal.dump the whole object. (That's not as terrible as it seems - marshalling is fast!)

#to_cache_key (danger!)

There's another way to define a cache key, but it should be used with caution because it gives you total control.

The key is to make sure your #to_cache_key method identifies both the class and the instance!

Comparison

Method Must uniquely identify class Must uniquely identify instance
#as_cache_key N† Y
#to_cache_key Y Y

† The class name is automatically inserted for you by calling object.class.name, which is what causes all the trouble with ActiveRecord::Associations::CollectionProxy, etc.

ActiveRecord

If you're caching methods on instances of ActiveRecord::Base, and/or using them as arguments to other cached methods, then you should probably define something like:

class ActiveRecord::Base
  def as_cache_key
    attributes
  end
end

If you find yourself passing association proxies as arguments to cached methods, this might be helpful:

# For use in ActiveRecord 3.0.x
class ActiveRecord::Associations::AssociationCollection
  # rare use of to_cache_key
  def to_cache_key
    [
      'ActiveRecord::Associations::AssociationCollection',
      proxy_owner.class.name,
      proxy_owner.id,
      proxy_reflection.name,
      conditions
    ].join('/')
  end
end

# For use in ActiveRecord 3.2.x
class ActiveRecord::Associations::CollectionProxy
  # rare use of to_cache_key
  def to_cache_key
    owner = proxy_association.owner
    [
      'ActiveRecord::Associations::CollectionProxy',   # [included because we're using #to_cache_key instead of #as_cache_key ]
      owner.class.name,                                # User
      owner.id,                                        # 'seamusabshere'
      proxy_association.reflection.name,               # :comments
      scoped.where_sql                                 # "WHERE `comments`.`user_id` = 'seamusabshere'" [maybe a little bit redundant, but play it safe]
    ].join('/')
  end
end

Otherwise, cache_method will call user.comments.class.name which causes the proxy to load the target, i.e. load all the Comment objects into memory. You probably don't want to load 1000 AR objects just to generate a cache key.

Generational caching

Generational caching allows clearing the cached results for only one method, for example

my_blog.cache_method_clear :entries

You can disable it to get a little speed boost

CacheMethod.config.generational = false

Generational caching stores a separate generation key that never expires by default. To set a default TTL for these

CacheMethod.config.default_generational_ttl = 120

Debug

CacheMethod can warn you if your obj or args cache keys are suspiciously long.

require 'cache_method'
require 'cache_method/debug'

Then watch your logs.

Module methods

You can put #cache_method right into your module declarations:

module MyModule
  def my_module_method(args)
    # [...]
  end
  cache_method :my_module_method
end

class Tiger
  extend MyModule
end

class Lion
  extend MyModule
end

Rest assured that Tiger.my_module_method and Lion.my_module_method will be cached correctly and separately. This, on the other hand, won't work:

class Tiger
  extend MyModule
  # wrong - will raise NameError Exception: undefined method `my_module_method' for class `Tiger'
  # cache_method :my_module_method
end

Contributors

Copyright 2012 Seamus Abshere