Space Vatican

Ramblings of a curious coder

Reload Me, Reload Me Not

Rails’ development[1] mode automatic reloading is pretty nifty. It would really suck having to restart the server everytime you made a change, both in terms of the seconds wasted each time, the manual pressing of buttons you’d have to do and the 5 minutes wasted here and there when you forgot to restart. There’s no point reloading rails itself though, so all that stuff stays around. No point reloading plugins either really. I should probably point out that reloading is a misnomer: it implies things are actively loaded (which they aren’t). What actually happens is sort of that Rails forgets that it has seen Customer and loaded customer.rb, so when it hits Customer it goes off down the const_missing chain and loads your file again.

Although if you were working on a plugin or trying to diagnose a problem with a plugin it could be useful, but maybe that’s an edge case. That’s until your plugin starts to contain something with long lived reference to one of your application’s classes (or an instance thereof). A classic example is model classes with associations going over the plugin/app boundary. A quick trip to the console shows the problem:

I’ve got a teeny tiny app with 2 models: messages and conversations (think forums): conversations have many messages.

1
2
3
4
5
6
7
8
9
10
>> c = Conversation.find :first
=> #<Conversation id: 1, title: "hello world", created_at: "2008-05-28 09:03:58", 
updated_at: "2008-05-28 09:03:58">
>> c.messages
=> []
>> reload!
Reloading...
=> true
>> c.messages
NoMethodError: undefined method `messages' for #<Conversation:0x1849ccc>

reload! makes Rails do its class reloading (a useful trick in itself if you’re fiddling around at the console and want to see some changes). But what happened? We had a perfectly good instance of conversation and then it got trashed! Lets take a closer look:

1
2
3
4
5
6
7
8
9
10
11
12
13
>> first_class = Conversation
=> Conversation(id: integer, title: string, created_at: datetime, updated_at: datetime)
>> first_class.object_id
=> 13302014
>> first_class.instance_methods - ActiveRecord::Base.instance_methods
=> ["message_ids", "message_ids=", ... ]
>> reload!
Reloading...
=> true
>> first_class.instance_methods - ActiveRecord::Base.instance_methods
=> []
>> Conversation.object_id
=> 13772244

So we stash away the conversation class. It’s got an object id, the methods we would expect, all good. Then we reload! and all the methods are gone. If you ask for Conversation again you get a different class! This is how class reloading of ActiveRecord classes works: the old class is gutted and the constant removed, but we can’t stop people hanging onto references to the old class [3](incidentally if you ever get an incomprehensible message about methods not existing when you can see the method definitions right in front of you then you’ve probably run into a variant of this).

So if you’ve got a plugin holding onto a reference to some class, this is what is likely to happen to it, nonsensical messages about methods not existing when they really should. Maddeningly, the first time you load a page it will load fine, but refresh it and it’s gone! Your tests will pass too, and if you’re even half sane then your app dying even though the tests pass will give you a bad feeling. If you’re lucky the only bad thing will be that changes won’t be noticed until a restart, but even that’s quite annoying (especially if you didn’t expect it — application classes are reloaded after all !).

The way out

The obvious way out is to reload the plugin as well[4]. The set of things that are loaded only once is controlled by Dependencies.load_once_paths [5](except for rails itself, that’s special) and by default plugin lib directories are added to it. As long as you load via the rails dependency mechanisms, any constant loaded from a path not in that array will be reloaded after a request. This is why a ruby style require of application classes is usually a bad thing: it can stop rails from reloading some of your classes which leads to the problems seen above.

In my dummy app i’ve got

1
2
>> Dependencies.load_once_paths
=> ["/Users/fred/empty_app/vendor/plugins/dummy_plugin/lib"]

There’s an easy way out: in the plugin’s init.rb just stick [2]

1
Dependencies.load_once_paths.delete(File.expand_path(File.dirname(__FILE__))+'/lib')

If we boot up our sample app again:

1
2
>> Dependencies.load_once_paths
=> []

Job done!

[1]None of this applies if config.cache_classes is true (for example in production mode).

[2]If you load plugins from non standard locations, you may have to fiddle with that: the string you delete must exactly match what rails put in Dependencies.load_once_paths, that it corresponds to the same location on disk is not enough.

[3]This does mean I was lying slightly when I said this could be handy if you’re working on a plugin: if what your plugin is doing is extending some rails base class with a module, then this won’t help. Rails won’t have stripped the methods from the module (so nothing will break), but ActionController (for example) will still have the old module included in it and so won’t see the changes.

[4]Another lie. Aren’t I naughty? The classes from the plugin’s lib folder will be reloaded as needed but the plugin’s init.rb won’t be run again (which in a way is the reason for the previous caveat: we never include the new module into ActionController. Rerunning init.rb would be messy though, apart from anything else it wouldn’t unwind the changes we made, so if we removed some methods from our module they would still be in ActionController).

[5]Dependencies is moving to the ActiveSupport namespace, so replace every occurrence of Dependencies with ActiveSupport::Dependencies if you are running > 2.1.