rails: Rails.application.eager_load! regression in Zeitwerk autoloader mode

https://github.com/rails/rails/blob/master/railties/lib/rails/engine.rb#L474 there is a note

Already done by Zeitwerk::Loader.eager_load_all in the finisher.

This doesn’t appear to be true outside of production modes.

It’s fairly common to eager load (Rails.application.eager_load!) for rake tasks or other reason in lower environments but the behavior with zeitwerk is different than in classic mode.

I believe adjusting the line beneath the comment from return if Rails.autoloaders.zeitwerk_enabled?

to return Zeitwerk::Loader.eager_load_all if Rails.autoloaders.zeitwerk_enabled? returns the behavior to the previous, but I’m unsure of other implications.

Steps to reproduce

rails new autoload
cd autoload
rails c
>
Rails.application.eager_load!
> nil
ActiveRecord::Base.descendants
> []

Expected behavior

in same app as above - edit config/application.rb add config.autoloader = :classic directly beneath config.load_defaults 6.0 save & exit

rails c
>
Rails.application.eager_load!
> ["/home/tongboy/autoload/autoload/app/channels", "/home/tongboy/autoload/autoload/app/controllers", "/home/tongboy/autoload/autoload/app/controllers/concerns", "/home/tongboy/autoload/autoload/app/helpers", "/home/tongboy/autoload/autoload/app/jobs", "/home/tongboy/autoload/autoload/app/mailers", "/home/tongboy/autoload/autoload/app/models", "/home/tongboy/autoload/autoload/app/models/concerns"]
ActiveRecord::Base.descendants
> [ApplicationRecord(abstract)]

similarly - if classic mode is not enabled and instead just Zeitwerk::Loader.eager_load_all is called rather than the Rails.application.eager_load! then the ‘good’ behavior is also seen.

To me this looks like a regression in the behavior of Rails.application.eager_load! in non-classic mode - matching the long-standing behavior seems to make the most sense, but I’m far from an expert in auto loading and just working on fixing up some gems for rails 6 support.

Please let me know if pulling together a PR or any additional information around this would be helpful.

System configuration

Rails version: 6.0.0

Ruby version: 2.6.3p62

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 14
  • Comments: 21 (18 by maintainers)

Most upvoted comments

Just to add some context, this is important if you have anything that does app-wide reflection (like rebuilding search indexes or reflecting on associations).

Bump on this. We used (past tense!) reflection quite a lot but on moving to Rails 6 under Zeitwerk, it all went wrong (dev mode - not in Production, thankfully). We even had cases as severe as this:

  • A base class defines some mandatory methods that all just raise “NotImplementedError”
  • Subclasses implement (override) all of those methods
  • An iterator over the base class .descendants is used in a test which instantiates the subclasses and calls one of the methods under test
  • When we ran these unit tests individually, all was fine
  • When we ran the whole suite, we’d get intermittent NotImplementedError exceptions - different every time, by seed - but this is impossible, unless somehow .descendants had enumerated the base class not a descendant, indicating a pretty horrible Zeitwerk failure.

We could’ve just eager-loaded the whole application, but that’s very slow. It’s when we first realised that Rails.application.eager_load! didn’t work anymore, too; fortunately this very bug report led us to the solution of using Zeitwerk::Loader.eager_load_all. We tried using require and a few other tricks that worked under the classic autoloader, but it just didn’t help. In the end we resolved it by losing a benefit of Ruby - we stopped doing reflection, instead hand-maintaining lists of subclasses in constants which are iterated over instead. This works reliably.

We are sticking with Zeitwerk since it’s recommended for Rails 6, but only with a sense of unease - we’ve seen too many weird bugs in dev that don’t happen if you change to the classic autoloader. This is a new application built under Rails 5.2 just a couple of months before 6.0 was released and I’m pretty sure we’re not doing anything particularly strange 😃 - just feels like Zeitwerk wasn’t quite ready for prime-time.

Fingers crossed for some fixes soon. If there’s anything that could be added here which might aid debugging at least this core issue with Rails.application.eager_load!, let me know.

@fxn No problem 🤝 Anyway, thanks for you hard work in Zeitwerk and its integration in Rails 🏆 It`s a great gem

You can put this in an initializer and not change any of your code:

module Rails
  class Application
    def eager_load!
      Zeitwerk::Loader.eager_load_all
    end
  end
end

It’s not an ideal solution, but it should ease migration without you having to change a bunch of reflection.

No activity in a year. I’ll close.

This is on our production system, so I won’t be able to for a few days, but I added this workaround for the time being:

    mongo_models = `find app/models -type f | while read line; do egrep -H "Mongoid::Document" "$line"; done`
    mongo_models.split("\n").each do |line|
      name = line.split(':')[0].split('/')[-1].split('.')[0].capitalize
      name.constantize
    end

This then loads all my models, but is a pretty messy solution 😦

@pedrofurtado yeah, sorry but took a Christmas hiatus. It’s in my plate.