It all started with one innocent refactoring. We had two classes defined in the same file. Say
Foo::Bar. Naturally, as
Foo lives in
app/models/foo.rb the place for
Foo::Bar is to be
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
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_missingcan’t locate its own ass let alone my poor
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
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
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
The solution I came up with was to require
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:
requirein 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?
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
The method implementation is quite a bit different in Rails 4, but the process is essentially the same:
nestingparameter. I’m not sure how exactly this parameter is passed, if ever. So it’s safe to assume that
nil. Note that
nestingis also a special method in pry which makes it a little tricky to inspect.
niltry to derive nesting from the parent scope name -
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 is a built in Ruby method that
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
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.