Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Ruby on Rails spuriously raise "Unable to Autoload Constant" after editing code?

Autoloading (and re-loading) generally works fine in my Ruby (2.3.0) on Rails (5.0.1) project. However, in development mode I occasionally see an error like the following:

Unable to autoload constant Foo::Bar, expected /app/models/foo/bar.rb to define it

This is unexpected because:

  1. The first request worked fine (it autoloaded once already).
  2. It only appears after editing code and sending a new request.
  3. It does not always happen. I cannot figure out why sometimes it fails to reload.
  4. The file (foo/bar.rb) does, in fact, define Foo::Bar.

Moreover, the code for foo/bar.rb is dead simple:

module Foo
  class Bar < CustomRecord
  end
end

The simple work-around is to restart the server and then send the request again (which always succeeds). FWIW, I'm using zeus server.

My best-guess is that something is not being reloaded, but I'm not sure how to go about debugging. I can't seem to point to any specific action which causes the problem. Sometimes editing code causes it to happen, sometimes not.

like image 332
Zane Claes Avatar asked Jan 04 '17 15:01

Zane Claes


1 Answers

There are a couple of good explanations for Rails autoloading behavior that you can find. This answer provides a useful explanation, which it grabs from this blog post at urbanautomaton.com.

Summary:

Basically, Rails first uses Ruby's constant lookup. If that fails, then it has its own lookup, which will miss files if

  • They are not in the autoload paths (Rails won't find app/foo/bar.rb if config.autoload_paths += %W(#{config.root}/foo) is not added to config/application.rb)
  • The directory name/namespace convention isn't followed

It will find Foo::Bar in app/foo/bar.rb if it's defined as:

module Foo
  class Bar
  end
end

but not

module Foo
  module Bar
  end
end
  • A constant in a different namespace is referenced without that namespace indicated in another file that's already been autoloaded

Quote from the blog post describing what I mean:

At this point, we’ve only seen how a single constant name maps to a single file name. But as we know, a constant reference in Ruby can resolve to a number of different constant definitions, which vary depending on the nesting in which the reference was made. How does Rails handle this?

The answer is, “partially”. As Module#const_missing passes no nesting information to the receiver, Rails does not know the nesting in which the reference was made, and it must make an assumption. For any reference to a constant Foo::Bar::Baz, it assumes the following:

module Foo
  module Bar
    Baz # Module.nesting => [Foo::Bar, Foo]
end

end

In other words, it assumes the maximum nesting possible for a given constant reference. The example reference is therefore treated exactly the same as the following:

Foo::Bar::Baz # Module.nesting => []

module Foo::Bar
  Baz # Module.nesting => [Foo::Bar]
end

While there’s been a significant loss of information, Rails does have some extra information it can use. It knows that Ruby failed to resolve this particular constant reference using its regular lookup, meaning that whatever constant it should refer to cannot be already loaded.

When Foo::Bar::Baz is referred to, then, Rails will attempt to load the following constants in turn, until it finds one that is already loaded:

  • Foo::Bar::Baz
  • Foo::Baz
  • Baz

As soon as an already-loaded constant Baz is encountered, Rails knows this cannot be the Baz it is looking for, and the algorithm raises a NameError.

It's difficult to say what exactly is causing your problem(s) without more specifics about how your code is set up, but hopefully understanding what's going on with autoloading in Rails might help a little. I would highly recommend reading through the blog post and answer linked above.

Edited for possible solution:

Make sure 'app/models/foo' is added to your autoload_load paths in config/application.rb - or move 'foo' to app/models/concerns, which should be there by default.

Often adding explicit require statements helps - especially if things are loading properly in development but not in test. I recently solved this problem by adding a file that required all of my nested files (I had a directory structure that did not mirror my namespace structure) and then requiring that file in my test helper.

Second edit

Including information relevant to the question, why a constant might load successfully only once.

From the same blog post:

If constants are loaded only when they’re first encountered at runtime, then by necessity their load order depends on the individual execution path. This can mean that the same constant reference resolves to different constant definitions in two runs of the same code. Worse still, the same constant reference twice in a row can give different results.

Let’s go back to our last example. What happens if we call .print_qux twice?

> Foo::Bar.print_qux
I'm in Foo!
=> nil
> Foo::Bar.print_qux

NameError: uninitialized constant Foo::Bar::Qux This is disastrous! First we’ve been given the wrong result, and then we’ve been incorrectly told that the constant we referred to doesn’t exist. What on earth led to this?

The first time, as before, is down to the loss of nesting information. Rails can’t know that Foo::Qux isn’t what we’re after, so once it realises that Foo::Bar::Qux does not exist, it happily loads it.

The second time, however, Foo::Qux is already loaded. So our reference can’t have been to that constant, otherwise Ruby would have resolved it, and autoloading would never have been invoked. So the lookup terminates with a NameError, even though our reference could (and should) have resolved to the as-yet-unloaded ::Qux.

We can fix this by referring to ::Qux first, ensuring that it’s loaded for Ruby to resolve the reference:

> Qux
=> "I'm at the root!"
> Foo::Bar.print_qux
I'm at the root!
=> nil
> Foo::Bar.print_qux
I'm at the root!
=> nil

A funny thing has happened here. In order to get correct behaviour, we deliberately loaded the constant we needed before we used it (albeit indirectly, by referring to it, rather than loading the file that defined it).

But wait; isn’t this suspiciously close to explicitly loading our dependencies with require, the very thing autoloading was supposed to save us from?

To be fair, we could also have fixed the issue by fully qualifying all of our constant references, i.e. making sure that within .print_qux we referred to ::Qux and not the ambiguous Qux. But this still costs us our existing intuitions about Ruby’s behaviour. Moreover, without intimate knowledge of the autoloading process, we would have been hard pressed to deduce that this was necessary.

like image 156
wendybeth Avatar answered Oct 12 '22 23:10

wendybeth