Building a large-scale web application can be troublesome. It leaves us facing all sorts of optimization problems. A very good – but no easy – solution is to use caching. Caching is a real saver as it can help with minimizing

  1. Database queries
  2. String operations

which are the two main problems that can affect the performance of a web application.

A common problem that developers always face is having to call a method regularly knowing that its results rarely change and its cost is always high. More on that in the following code

class User < ActiveRecord::Base
  def costy_method
    #some real heavy calculation that takes alot of resources
    #to complete
    #this method is called regularly and its results rarely change
  end
end

Calling user.costy_method with all its costs and behavior appears to be a point that can be optimized. How about caching its result? The user.costy_method needs to be modified so that it caches its result. A common practice for Ruby developers is to cache the result in an instance variable the following way

def costy_method
  @costy_method_result || = #the actual calculations
end

What if there were more methods like this? They’ll all have to be rewritten so as to cache their results. This would be a tedious task with a lot of repeated code which violates DRY concept. Why not have a simpler solution for this particular problem? We wrote the method_cache plugin. Let’s see how it works

class User < ActiveRecord::Base
  def costy_method
    #some real heavy calculation that takes alot of resources
    #to complete
    #this method is called regularly and its results rarely change
  end
  caches_method :costy_method
end

and voila, every time user.costy_method is called, you’ll get the value you expect normally but this time at a much less cost.
caches_method :costy_method builds a 2 level cache(a note on that later), it caches the result in an instance variable inside the user object and it also saves a copy in your fragment_cache_store which can be of several types (a file store, a memory store or memcached). Now if the same user instance is used to call user.costy_method we’ll get a huge performance gain as it will just cost u as much as it costs to retrieve any instance variable. If another user instance is used, the first time you call user.costy_method the value will be retrieved from your cache store and put for your convenience in the instance variable. If the value can’t be retrieved from an instance variable or the cache store the real costy_method implementation is executed to get that value.

Another convenience method is expire_method :method_name which can be used like user.expire_method :costy_method. This method will simply clear the instance variable and cache store entry if they’re available so that next time user.costy_method is called, a new value will be calculated.

Mido – Eng. Muhammed Ali – wrote this plugin. I pretty much loved the simplicity by which this will allow us to cache stuff.

More on the plugin features
We needed to be able to cache the results for certain amount of time. The problem is that the default MemCacheStore didn’t support that. So again with some Ruby magic I overrided the MemCacheStore code so that I can write some stuff like the following
caches_method :costy_method, :until => :midnight
or
caches_method :costy_method, :for => 20.days

After using the plugin internally for a while we decided it’s best if others can make use of it and also contribute. We’ve setup a project on RubyForge which can be found here.

In its current form, the plugin provides the following extensions to ActiveRecord Objects
caches_method :method_name
caches_class_method :method_name
and for expiring the cache
instance.expire_method :method_name
Class.expire_instance_method :method_name, id
Class.expire_class_method :method_name

More on the 2 level cache mentioned before, for class methods, we cached the results in static variables but this was a deal breaker once we used several mongrels to serve our application as it’s impossible to synchronize the expiry of the static variables on the different mongrels so we decided it was enough to use the fragment cache store as our only store when dealing with class methods.

I think it would be nice to refactor the plugin so as to be able to use it with all classes not just ActiveRecord::Base descendants. This would require some minor changes such as defining a method that defines an instance uniquely. In case of ActiveRecord objects, it’s just the id.

Update
I’ve updated the plugin so it uses the Rails cache store introduced in Rails 2.1. I’ve also moved the plugin to GitHub and it can be found here.

To install it

ruby script/plugin install git://github.com/humanzz/method_cache.git
Technorati Tags: ,,