Improvements on Caches plugin for Rails

Google Buzz

On of the nicest and simplest caches available, that you learn right on the classes is the method cache for Lisp.

This type of cache just overwrites the function definition, injecting some code. This code generates a static hash that stores the function results. The hash key is of course some unique key for args passed. The new method, before running, now verifies if it as any value on the cache regarding the params received. If it does, it immediately returns the cache, if not it runs the method as before, saving the returned value on the hash.

This is great for costy methods and on Rails there is a nice Cache.rb plugin that does this. You can easily change the plug-in to run with MemCache.

At Masterfoot we use this to cache expensive methods like team rankings, global rankings, players goals historial, etc..
All these functions have more that 2 years of info in a somehow complex Model to deal with. Some of those can take to about 4 min. to perform and no one wants to wait that much.

So, easily and elegantly, we run this methods on environment start on background and after that all is quick and fun.

Only two problems, on the first 4 minutes, whenever someone goes to visit a page that has any ranking information – almost every page – a new mongrel will probably start calculating the all thing again in parallel and the number of mongrels you have is for sure the number of times a very expensive method will be called in a busy site. Your CPU will probably beat some Hot Chip songs and not respond to anyone in some minutes.
The other problem is that if you set any match score, you will have to calculate the entire rankings again as any change could happen. During those minutes, your site can go down again.. or worse.

To resolve this a simple pair of methods are injected on class cached method. Here is how-to:

The first: {#method_cached}_cached will only return the method value if it is already cached. If you handle the nil properly this should work fine. In our case, we present some info stating that the ranking is being updates.. so that the use can come in some moments.
Here is the simple code to put on ‘caches’ method after ‘define_method(“#{name}”)’ block:

      define_method("#{name}_cached") do |*args|
        self.cachesrb_cache ||= {}
        key = cachesrb_method_key(name,*args)
        got_value = false
        until got_value
          begin
            cached = self.cachesrb_cache[key]
            got_value = true
          rescue ArgumentError
            begin
              $!.message.split.last.constantize
            rescue NameError
              throw $!
            end
          end
        end
        unless cached
          nil
        else
          unless Time.now.to_i > cached[:expires_at]
            cached[:value]
          else
           nil
          end
        end
      end 

But presenting this type of message every time we set a new score is just a pain in the ass.. for the user, that is.
So a second method is generated dynamically: {#method_cached}_refresh
Add it just after the one before:

      define_method("#{name}_refresh") do |*args|
        self.cachesrb_cache ||= {}
        key = cachesrb_method_key(name,*args)
        val = self.send(saved_getter.to_sym,*args)
        self.cachesrb_cache[key] = { :value => val, :expires_at => Time.now.to_i + options[:timeout]}
        return val if self.cachesrb_cache[key].nil?
        return self.cachesrb_cache[key][:value]
      end 

This will not invalidate the method result to start from. It will recalculate the result and only after update the hash value :)
With this two methods, class / method caching is much more elegant and smooth.



 
Was it any good?

Add to Technorati Favorites

AddThis Social Bookmark Button

Add 
to Mixx!