Saturday 28 June 2008

Rails 2.1 caching - nothing is ever easy!

Last night I watched the new Railscast episode that talked about the new caching features in Rails 2.1. I thought it looked cool and would add it to the new round of FilmAmora changes.

But... nothing is every easy!

I may look simple, but I encountered several problems

1. Conflicts with GetText

We use GetText for the translations on FilmAmora. We like it because there are free poEditor apps on every platform and we can easily send off the files to whomever to be translated.

The problem comes with this line:
require 'gettext/rails'


That you need to have to fire up some of the Rails-specific GetText stuff. This is all fine - it has been working for quite some time. But, when I tried to cache our Genres like this:
  def self.all_cached(language)
key = "genres_#{language}"
Rails.cache.fetch(key) {Genre.find(:all).sort_by {|genre| genre.get_description}.reject {|g| g.get_films_count == 0}}
end


Here is the result:
undefined method `cache' for GetText::Rails:Module


What?!?! Yes, it seems that GetText::Rails will hide Rails. This is, quite frankly, SHIT. So, after a long time poking around I have discovered that you need to do this:

  def self.all_cached(language)
key = "genres_#{language}"
::Rails.cache.fetch(key) {Genre.find(:all).sort_by {|genre| genre.get_description}.reject {|g| g.get_films_count == 0}}
end


Yippee! It will all work now, right?

Wrong.

2. Class != Class

I hit refresh the first time and wow was I excited! I saw lines like this in the log:
Cache write (will save 0.59562): genres_es


Woo hoo! Look at all the time I will save!

So I hit refresh.
undefined method `get_description' for #<Genre id: 1, description: "Action and Adventure", order_by: 2>


What?!?!

I am using the 'default' memory store. Something is going funny with retrieving objects from it. I never solved this problem.

If I do this in the console:
>> @genres = Genre.all_cached("es")
>> @genres = Genre.all_cached("es")


I see this in the log:
Cache write (will save 0.51974): genres_es
Cache hit: genres_es ({})

And I can do this:
>> @genres[0].get_description("es")


So what is going on in my web app? I have no idea. It seems that the class retrieved from the memory cache is incomplete in some way. get_description is not an accessor, it does go off and get the translation, but... so what? it is still a method.
This had me stumped!

3. File Store no worky
So I added this to development.rb:
config.cache_store = :file_store, '/cache_store'

I ended up getting this:
undefined method `get_description' for #<String:0x5459f0c>


Ok, I am tired of this now.

4. MemCached
Everyone is talking about using MemCached for this kind of thing. Now, I know developers and we are a lazy bunch. My guess is that this whole caching stuff has been written with MemCache in mind and screw anything else (see points 2 and 3 above).
So I installed MemCache and changed the line in the development.rb to be this:
config.cache_store = :mem_cache_store


I restarted the server and hit refresh. Trying to contain my excitement I saw a properly rendered page.
I hit refresh again.
undefined class/module Subgenre

For CHRIST'S SAKE!
After another web-scouring exercise I discovered a solution.

My method now looks like this:
  def self.all_cached(language)
key = "genres_#{language}"
Subgenre
::Rails.cache.fetch(key) {Genre.find(:all).sort_by {|genre| genre.get_description}.reject {|g| g.get_films_count == 0}}
end


And guess what? It works. Of course it now means running memcache on my local machine and installing it on the production box. But I've saved .52 seconds! It took 3 hours to get to the solution, so I figure in only 350 web page hits I will make it back.

Oh, of course I was already caching the html on the server for most things, but this is a little tidier.

4 comments:

Rikas said...

I'm having the exact same problem here.

But I don't want to use memcached. Is there a solution?

Ari the Brown said...

for your mysterious "works once bug", it could be a few things. http://www.spacevatican.org/2008/9/28/required-or-not

I've run into this bug many times. The link above works - just remove all manual require's and it should disappear. If the error stems from method missing (with a stack level too deep), it's because of (for me, at least), a bug with defining #id in ActiveRecord.

Good luck!

Shak said...

Did you ever figure out your second problem? My reloaded-from-cache-objects seem to lose defined methods too, so I'd be interested if you managed to get past this!

kikito said...

Hi everyone,

I managed to find a work-around on the second problem, and I'm not using memcache.

But you might not like the solution: I convert the objects to strings before caching them, and parse them when retrieving.


def self.all_cached(language)
key = "genres_#{language}"
Subgenre
str = ::Rails.cache.fetch(key) do
Marshal.dump(
Genre.find(:all).sort_by { |genre| genre.get_description}.reject {|g| g.get_films_count == 0}
)
end
Marshal.load(str)
end