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:paramslist of params to pass to the storeconditionsis then passed to the store so any conditions supported by the store (eg.:expire_infor 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.
-
targetcan be of the form[controller, action]if no controller is given, the current controller is used -
conditions accepts the following specific parameters
:urithe uri of the resource you want to eager cache (it’s needed by the page store but can be provided instead by a block):methodthe http method to use when requesting the resource to eager cache (defaults to :get):storewhich store to use foreager_caching:paramslist 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)
-
targetcan be of the form[controller, action]if no controller is given, the current controller is used -
conditionsaccepts the following specific parameters:urithe uri of the resource you want to eager cache (it’s needed by the page store but can be provided instead by a block):methodthe http method to use when requesting the resource to eager cache (defaults to :get)
-
paramsis just passed as a parameter to the block -
envis used to create a new request if you do not pass a block (it’s passed tobuild_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.