Merb-cache's methods

Posted by Guillaume Maury
on Dec 19, 08

Introduction

Important note

A lot of this code needs the version of merb-cache found at http://github.com/benschwarz/merb-cache. There have been a few patches made to it particularly for eager_cache and fetch_partial

Beware that the code and api might change, I will keep this article up-to-date with the changes..

The cache controller method

cache *actions, conditions

Caches the result of the action. Accepts two specific options:

  • :store (or :stores) use the specified store
  • :params list of params to pass to the store conditions is then passed to the store so any conditions supported by the store (eg. :expire_in for MemcachedStore) can be used.
  def cache(*actions)
    conditions = extract_options_from_args!(actions) || {}
    actions.each {|a| cache_action(a, conditions)}
  end

  def cache_action(action, conditions = {})
    before("_cache_#{action}_before", conditions.only(:if, :unless).merge(:with => [conditions], :only => action))
    after("_cache_#{action}_after", conditions.only(:if, :unless).merge(:with => [conditions], :only => action))
    alias_method "_cache_#{action}_before", :_cache_before
    alias_method "_cache_#{action}_after",  :_cache_after
  end

As you can see it adds a before filter and an after filter.

The before filter (_cache_before)

  def _cache_before(conditions = {})
    unless @_force_cache
      if @_skip_cache.nil? && data = Merb::Cache[_lookup_store(conditions)].read(self, _parameters_and_conditions(conditions).first)
        throw(:halt, data)
        @_cached = true
      else
        @_cached = false
      end
    end
  end
  
  def _lookup_store(conditions = {})
    conditions[:store] || conditions[:stores] || default_cache_store
  end

@_force_cache is set by the force_cache! instance method which is called when eager caching (see below) @_skip_cache is set by the skip_cache! instance method

You can override default_cache_store to set a default store for your controller

The after filter (_cache_after)

  def _cache_after(conditions = {})
    if @_cached == false
      if Merb::Cache[_lookup_store(conditions)].write(self, nil, *_parameters_and_conditions(conditions))
        @_cache_write = true
      end
    end
  end

Pretty trivial it just writes gives the current instance of the controller to the store write method (for more details on what happens then see http://gom-jabbar.org/articles/2008/12/09/merb-cache-and-its-stores) and uses parametersand_conditions(conditions) to determine which parameters and conditions to pass on to the store

_parameters_and_conditions(conditions)

This methods is used to determine which parameters will be passed to the cache store.

def _parameters_and_conditions(conditions)
  parameters = {}

  if self.class.respond_to? :action_argument_list
    arguments, defaults = self.class.action_argument_list[action_name]
    arguments.inject(parameters) do |parameters, arg|
      if defaults.include?(arg.first)
        parameters[arg.first] = self.params[arg.first] || arg.last
      else
        parameters[arg.first] = self.params[arg.first]
      end
      parameters
    end
  end

  case conditions[:params]
  when Symbol
    parameters[conditions[:params]] = self.params[conditions[:params]]
  when Array
    conditions[:params].each do |param|
      parameters[param] = self.params[param]
    end
  end

  return parameters, conditions.except(:params, :store, :stores)
end

It passes by default all the parameters used by merb-action-args (accessed by self.class.action_argument_list[action_name]) and then uses the :params condition to determine which parameters to pass.

eager_cache

Eager caching is one solution to the dog pile effect (see Introducing Mint Store’s introduction for a quick introduction to the dog pile effect). Eager cache basically caches some actions whenever the trigger action is completed. It uses Merb.run_later so as not to make the client wait. There are two eager_cache methods, an instance method and a class method

The class method

eager_cache(trigger_action, target = trigger_action, conditions = {}, &blk)

After trigger_action has been run, the target action will be cached with the conditions sent to the store.

  • target can be of the form [controller, action] if no controller is given, the current controller is used

  • conditions accepts the following specific parameters

    • :uri the uri of the resource you want to eager cache (it’s needed by the page store but can be provided instead by a block)
    • :method the http method to use when requesting the resource to eager cache (defaults to :get)
    • :store which store to use for eager_caching
    • :params list of params to pass to the store when writing to it.

    Of course, conditions is also passed to the store, so any other supported conditions from the store can be used.

This method accepts a block that allows you setup the request (more on this later).

The instance method

eager_cache(action, conditions = {}, params = request.params.dup, env = request.env.dup, &blk)

  • target can be of the form [controller, action] if no controller is given, the current controller is used

  • conditions accepts the following specific parameters

    • :uri the uri of the resource you want to eager cache (it’s needed by the page store but can be provided instead by a block)
    • :method the http method to use when requesting the resource to eager cache (defaults to :get)
  • params is just passed as a parameter to the block

  • env is used to create a new request if you do not pass a block (it’s passed to build_request)

This method accepts a block that allows you setup the request (more on this later).

The heart of eager caching the eager_dispatch class method

This is the function that is called in the run_later block

def eager_dispatch(action, params = {}, env = {}, blk = nil)
  kontroller = if blk.nil?
    new(build_request({}, env))
  else
    result = case blk.arity
      when 0  then  blk[]
      when 1  then  blk[params]
      else          blk[*[params, env]]
    end

    case result
    when NilClass         then new(build_request({}, env))
    when Hash, Mash       then new(build_request({}, result))
    when Merb::Request    then new(result)
    when Merb::Controller then result
    else raise ArgumentError, "Block to eager_cache must return nil, the env Hash, a Request object, or a Controller object"
    end
  end

  kontroller.force_cache!

  kontroller._dispatch(action)

  kontroller
end

So as you can see the block you give is used to create the controller instance. You can either return a hash that will be used as the env for the build_request, return a request or return a controller.

Once the kontroller instance is created, force_cache! is called to bypass the cache read in _cache_before and the action is dispatch to the controller.

Examples of using eager_cache

Class method:

Eager caching show and index after creating:

  class Articles
    cache :show, :index
    eager_cache :update, :show do |params|
      self.build_request(build_url(:article, :id => params[:id]), :id => 1)
    end
    eager_cache :update, :index, :uri => '/articles'
    eager_cache :create, :index
    eager_cache(:create, [Timeline, :index]) {{ :uri => build_url(:timelines)}}
    eager_cache(:update, :show) do |params| 
      build_request(build_url(:article, :id => params[:id]), :id => params[:id])
    end

  end

Let’s analyse them one by one:

  eager_cache :update, :index, :uri => '/articles'

When the update action is completed, a get request to :index with ‘/articles’ uri will be cached (if you use the page store, this will be stored in ‘/articles.html’)

  eager_cache :create, :index

This does the same after the create action but uses the fact that if no uri is given, the current uri is used but the http method used is get. This defaults work well with standard resource controller….

  eager_cache(:create, [Timeline, :index]) {{ :uri => build_url(:timelines)}}

This line is equivalent to eager_cache(:create, [Timeline, :index]) { build_request(build_url(:timelines))} but is a bit more readable in my oppinion. It shows how you can use the url generation for specifying the uri.

  eager_cache(:update, :show) do |params| 
    build_request(build_url(:article, :id => params[:id]), :id => params[:id])
  end

This eager caches the show action for the updated object. It’s possible to do this, but I think using the instance method (see below) for this task is a cleaner approach.

Instance method:

If you want to cache the show action for a newly created article, you’ll need to use the instance_method.

  def create(article)
    @article = Article.new(article)
    if @article.save
      eager_cache :show, :uri => url(:article, @article)
      redirect resource(@article), :message => {:notice => "Article was successfully created"}
    else
      #....
    end
  end

What I usually do is create a common private method (e.g. eager_cache_article) that I call from update and create.

fetch_partial

It caches the result of a partial.

def fetch_partial(template, opts={}, conditions = {})
   template_id = template.to_s
   if template_id =~ %r{^/}
     template_path = File.dirname(template_id) / "_#{File.basename(template_id)}"
   else
     kontroller = (m = template_id.match(/.*(?=\/)/)) ? m[0] : controller_name
     template_id = "_#{File.basename(template_id)}"
   end

   unused, template_key = _template_for(template_id, opts.delete(:format) || content_type, kontroller, template_path)
   template_key.gsub!(File.expand_path(Merb.root),'')

   fetch_proc = lambda { partial(template, opts) }
   params_for_cache = opts.delete(:params_for_cache) || opts.dup

   concat(Merb::Cache[_lookup_store(conditions)].fetch(template_key, params_for_cache, conditions, &fetch_proc), fetch_proc.binding)
 end

It is called the same as a partial would except that you can add a conditions hash at the end that will be passed to the cache store.

Caveat

If you call fetch_partial with parameters that are instance of model it will fail. Currently the cache stores available convert the parameters to string using to_s. And @model.to_s by default will give you something like #<Article:0x255ca9c>

So instead, you can either override to_s in your model (I don’t recommend it, it doesn’t express your intent clearly and will probably be hell to maintain later) or you can pass the :params_for_cache option which will be used by your cache store.

Another possibility is to create a specific strategy store for your project that takes care of converting your models. It’s useful if you use a technique like the one explained by Tobias Lutke in http://blog.leetsoft.com/2007/5/22/the-secret-to-memcached, you can create a strategy store that is in charge of finding the current store version (and passing it along as a parameter) from the parameters passed to it.

After this article, you might want to check Merb-cache and its stores

I’m currently available for hire on a contract basis.

blog comments powered by Disqus