Why does modifying my classes dynamically only work once in Rails?

(Or the downside of class caching)

This is a gotcha in Rails where your custom plugin or other dynamic change of a class only seems to work once.

First, a little background:
Recently we had a problem where we wanted an object to behave differently in two different instances of Rails. Ya see, we have models that are shared by two separately running rails instances. One is a cms (content management system) where all objects (let's say they are of class Thing) should show up. The other is the live site that should only show 'things' that have been designated 'live' (as opposed to, say, 'draft'). Now changing the search to filter out non-live 'things' was easy, but since you can call thing_instance.related_things and get back related objects of type Thing, now we have a problem. In the live site I want a call to 'related_things' to only bring back 'live' objects. But in the cms I want the same call to 'related_things' to bring back all the related objects no matter what the status.

Now I could create a new method called 'live_related_things' which wraps 'related_things' and filters but there's 3 problems with this:
  1. 'related_things' is used EVERYWHERE in the project – I'd have to change, like, a million files.
  2. Having 'related_things' and 'live_related_things' is really dumb. It's a bug waiting to happen.
  3. Hey this is ruby -- I can change the behavior of an class dynamically. Wooo!

So I created a plugin that uses alias_method_chain to change the behavior of the live site. And I felt all cool 'cause I was using THE POWER OF DYNAMIC LANGUAGES to solve a problem. And when I tried it in the app, it worked great. Once. On the second request it didn't work at all.

One of the cool things about Rails is that you can change your models and you don't have to restart your server -- really speeds up development. But how do they accomplish such magic? By tearing apart and rebuilding your models on every request. And the plugins only get loaded once. So when I started up my server the plugin loaded and everything was fine and on the second request the model was rebuilt from scratch (and the plugin was not reloaded) and it didn't work at all. Now this wouldn't happen if I was running in production mode (because its crazy inefficient to keep reloading classes on every request). So my solution was to add this line to my application.rb:

extend ThingExtension if Dependencies.mechanism == :load
'Dependencies.mechanism == :load' is true if rails is loading classes for every request and ThingExtension is a module that looks like this:

module ThingExtension
unless Thing.include?(::ThingLiveFilter)
load File.dirname(__FILE__) + '/../../local/plugins/live_filter/init.rb'
end
end

I have to call 'load' instead of 'require' because require only loads the file once and it's already been required when the project started up. 'Load' will open that file every time, but I need to check if Thing already has the ThingLiveFilter module included in it or I'll get one of those great 'stack level too deep' errors. Why? Well the when the project starts up, the plugin is called and then if 'load' calls it again alias_method_chain will get stuck in an infinite loop trying to redefine a method that's already been chained.

So there ya go, something to keep any eye out for when you're developing in Rails.

Update: if you're gonna do something like I described above AND cache your objects then you might want to read my bit on Caching Dynamically Modified Objects and the Trouble it Causes

Comments

Anonymous said…
There are ways around this besides setting config.cache_classes = true.
Steven R. Baker said…
This is just another victim of Rails' underdocumented magic. :(

Popular posts from this blog

Point Inside a Polygon in Ruby

What's a Good Flog Score?

SICP Wasn’t Written for You