Update: As @yves_senn and @fxn pointed out there’s a Rails Guide now on the subject of autloading and constants reloading. Kudos to @fxn!
It all started with one innocent refactoring. We had two classes defined in the same file. Say Foo
and Foo::Bar
. Naturally, as Foo
lives in app/models/foo.rb
the place for Foo::Bar
is to be app/models/foo/bar.rb
. As app/models/
is listed among autoload_paths
I thought nothing could go wrong. As soon as Bar
is referenced from inside the Foo
its path will be resolved and it would be loaded from app/models/foo/bar.rb
.
The change was tested and successfully deployed to production. The hit came from an unexpected side - Rake task using this code failed with Uninitialized constant Bar
.
The first question was “Why does it work in the UI but fails in the Rake task?” After some investigation I found out two facts:
const_missing
(more on that later);const_missing
can’t locate its own ass let alone my poor Foo::Bar
class.Given that, the fact that the app still worked when accessed from the web interface could mean only one thing - resolving Foo::Bar
never had to hit const_missing
in the first place. All thanks to eager loading.
Rails’s eager loading feature allows files (and constants defined in them) to be loaded on startup instead of loading “on demand” when a constant is first referenced. This means that when in production environment Foo::Bar
constant is first encountered it’s already loaded and doesn’t need to be found with const_missing
.
But if production Rake tasks run in the same production environment where eager loading is enabled then why it doesn’t work for them? For Rails 3.2 the reason could be found in finisher.rb
file:
Note the condition. Checking for config.cache_classes
is fair enough - it probably doesn’t make much sense to load everything on startup if you’re going to reload it on every change. But it also checks for the global variable $rails_rake_task
which if defined indicates that the code is run inside the Rake task!
Interestingly there are no such conditions in Rails 4.2. Instead you explicitly say whether or not to eager load classes with config.eager_load
option:
The solution I came up with was to require bar.rb
in foo.rb
to make sure Foo::Bar
is already loaded when it’s needed. I tested it in the development environment where eager loading was disabled and it seemed to work. Except it didn’t.
As soon as I changed anything at all in the app and reloaded the page I got same old Undefined constant Bar
exception. After thinking about it for a while I realized that it happened because of the combination of two factors:
require
in Ruby requires files only once. If the file was already loaded it will not be loaded again.So when I changed code in the app Rails removed all constants, but require
on top of the foo.rb
didn’t do anything because files had already been loaded. One way to solve this is to use load
in place of require
which will load a file every time the code is run. But I decided to get to the core of the const_missing
issue instead. Why can’t it find Bar
constant when it’s referenced from inside the scope it was defined in?
const_missing
First, read this brilliant piece on Ruby and Rails constant lookup and autoloading. It explains everything you should know about Rails autoload process.
Now let’s dig into const_missing
as it’s defined in ActiveSupport::Dependencies
module:
The method implementation is quite a bit different in Rails 4, but the process is essentially the same:
nesting
parameter. I’m not sure how exactly this parameter is passed, if ever. So it’s safe to assume that nesting
is nil
. Note that nesting
is also a special method in pry which makes it a little tricky to inspect.nesting
is nil
try to derive nesting from the parent scope name - klass_name
.const_name
and nesting.Now here’s the rough example of how the troublesome code looks:
What puzzled me is that Bar
is referenced from inside the scope where it’s defined, why const_missing
can’t detect that? And then it hit me. Look closer at const_missing
first line: klass_name = name.presence || "Object"
. What is name
?
Turns out name
is a built in Ruby method that
1
Returns the name of the module... Returns nil for anonymous modules.
And if we inspect self
at the point where name
is called it will show us #<Class:Foo::Bar>
which is a class object because Baz
is referenced from inside class << self
scope! And as class object is an anonymous module its name is nil
and const_missing
is trying to find top-level Baz
constant instead of Foo::Bar
. If instead of class << self
the method was defined as self.baz
the problem wouldn’t occur.
I don’t know if there’s a catchall solution to this kind of problems. Explicitly loading all dependencies seems to be to tedious for large Rails projects. Referencing constants by the full name all the time breaks encapsulation. IMO The best way to avoid constant lookup issues in all your environments is to
I hope this will save you some hair I had to pull off while trying to debug this issue.
TL;DR: Look out for classes referenced from inside class << self
.