Cacheable

By Splitwise

Cacheable is a gem which intends to adds method caching in an aspect-oriented programming (AOP) fashion in Ruby. Its core goals are:

  • ease of use (method annotation)
  • flexibility (simple adaptablability for any cache backend)
  • portability (plain Ruby for use with any framework)

While Rails is not a requirement, Cacheable was built inside a mature Rails app and later extracted. This first release will seemlyless work in Rails and only includes an adapter for an in-memory cache backed by a simple Hash. This may be enough for you needs but it is more likely that additional cache adapters will need to be written.

See more about Cache Adapters.

Getting Started

Add it to your Gemfile:

gem 'cacheable'

Set your cache adapter

# If you're in a Rails app place the following in config/initializers/cacheable.rb
Cacheable.cache_adapter = Rails.cache

# Otherwise you can specify the name of the adapter anywhere before you use it
Cacheable.cache_adapter = :memory

Simple Implementation Example

Cacheable is designed to work seemlessly with your already existings codebase. Consider the following contrived class:

class SimpleExample
  def expensive_calculation
    puts 'beginning expensive method'
    
    return 'my_result'
  end
end

To cache this method and it's result, simply add the following:

require 'cacheable' # this may not be necessary depending on your autoloading system

class SimpleExample
  include Cacheable

  cacheable :expensive_calculation

  def expensive_calculation
    puts 'beginning expensive method'
    
    return 'my_result'
  end
end

Thats it! There's some complex Ruby magic going on under the hood but to the end user you can simply call expensive_calculation and the result will be retreived from the cache, if available, or generated and placed into the cache. To confirm it is working, fire up an IRB console try the following:

> s = SimpleExample.new
> s.expensive_calculation
beginning expensive method
 => "my_result"
> s.expensive_calculation
 => "my_result"

# Notice that the `puts` was not output the 2nd time the method was invoked.

Additional Methods

Cacheable also adds two useful methods to your class.

Skip the Cache via #{method}_without_cache

The cache can intentionally be skipped by appending _without_cache to the method name. This invocation with neither check the cache nor populate it as if you called the original method and never used Cacheable.

> s = SimpleExample.new
> s.expensive_calculation_without_cache
beginning expensive method
 => "my_result"
> s.expensive_calculation_without_cache
beginning expensive method
 => "my_result"

Remove the Value via clear_#{method}_cache

The cache can be cleared at any time by calling clear_#{your_method_name}_cache.

> s = SimpleExample.new
> s.expensive_calculation
beginning expensive method
 => "my_result"
> s.expensive_calculation
 => "my_result"

> s.clear_expensive_calculation_cache
 => true
> s.expensive_calculation
beginning expensive method
 => "my_result"

Additional Configuration

Cache Invalidation

Default

One of the hardest things to do correctly is cache invalidation. Cacheable handles this in a variety of ways. By default Cacheable will construct key a key in the format [cache_key || class_name, method_name].

If the object responds to cache_key its return value will be the first element in the array. ActiveRecord provides cache_key but it can be added to any Ruby object or overwritten. If the object does not respond to it, the name of the class will be used instead. The second element will be the name of the method as a symbol.

It is up to the cache adapter what to do with this array. For example, Rails will turn [SomeClass, :some_method] into "SomeClass/some_method". For more information see the documentation on Cache Adapters

Set Your Own

If (re)defining cache_key does not provide enough flexibility you can pass a proc to the key_format: option of cacheable.

class CustomKeyExample
  include Cacheable

  cacheable :my_method, key_format: -> (target, method_name, method_args) do
    args = method_args.collect { |argument| "#{argument.class}::#{argument}" }.join
    "#{method_name} called on #{target} with #{args}"
  end

  def my_method(arg1)
    
  end
end
  • target is the object the method is being called on (#<CustomKeyExample:0x0…0>)
  • method_name is the name of the method being cached (:my_method)
  • method_args is an array of arguments being passed to the method ([arg1])

So if we called CustomKeyExample.new.my_method(123) we would get the cache key

"my_method called on #<CustomKeyExample:0x0…0> with Integer::123".

Conditional Cacheing

You can control if a method should be cached by supplying a proc to the unless: option which will get the same arguments as key_format:. Alternatively this method can be defined on the class and a symbol of the name of the method can be passed. Note: When using a symbol, the first argument will not be passed but will be available in the method as self. The following example will not cache the value if the first argument to the method is false.

class ConditionalCachingExample
  include Cacheable

  cacheable :maybe_cache, unless: :should_not_cache?

  def maybe_cache(cache)
    
  end

  def should_not_cache?(_method_name, method_args)
    method_args.first == false
  end
end

Cache Options

If your cache backend supports options you can pass them as the cache_options: option. This will be passed though untouched to the cache's fetch method.

cacheable :with_options, cache_options: {expires_in: 3_600}

Flexible Options

You can use the same options with multiple cache methods or limit them only to specific methods:

cacheable :these, :methods, :share, :options, key_format: key_proc, unless: unless_proc
cacheable :this_method_has_its_own_options, unless: unless_proc2

Class Method Cacheing

You can cache class methods just as easily as a Ruby class is just an instance of Class. You simply need to include Cacheable within the class << self block. Methods can be defined in this block or outside using the def self. syntax.

class StaticMethodExample
  class << self
    include Cacheable

    cacheable :class_method, :self_class_method

    def class_method
      puts 'class_method called'
    end
  end

  def self.self_class_method
    puts 'self_class_method called'
  end
end

Contributors (alphabetical by last name)