2

After researching this on SO and a very similar issue in Rails' GitHub issues, I'm still unclear what's wrong. My namespaced model subclasses are not eager-loaded, but I believe they are declared correctly and in the right place.

They do seem to be autoloaded and are accessible, but each one does not show up in subclasses of the parent class until they are instantiated.

The parent model:

# /app/models/queued_email.rb

class QueuedEmail < ApplicationRecord
end

My namespaced subclass models (there are a dozen):

# /app/models/queued_email/comment_notification.rb

class QueuedEmail::CommentNotification < QueuedEmail
end

# or alternatively (this also doesn't eager load):

module QueuedEmail
  class CommentNotification < QueuedEmail
  end
end

The relevant message from Rails.autoloaders.log! (in config/application.rb)

Zeitwerk@rails.main: autoload set for QueuedEmail, to be autovivified from /vagrant/rails_app/app/models/queued_email
Zeitwerk@rails.main: earlier autoload for QueuedEmail discarded, it is actually an explicit namespace defined in /vagrant/rails_app/app/models/queued_email.rb
Zeitwerk@rails.main: autoload set for QueuedEmail, to be loaded from /vagrant/rails_app/app/models/queued_email.rb

If I open rails console and call subclasses, i get nothing:

> QueuedEmail
 => QueuedEmail (call 'QueuedEmail.connection' to establish a connection) 
> QueuedEmail.subclasses
 []

But then... the subclass is accessible.

> QueuedEmail::CommentNotification
 => QueuedEmail::CommentNotification(id: integer...)
> QueuedEmail::CommentNotification.superclass
 => QueuedEmail(id: integer...)
> QueuedEmail.subclasses
 => [QueuedEmail::CommentNotification(id: integer...)]

I get nothing in subclasses until each one is instantiated in the code. Is my app/models folder incorrectly organized, or my subclasses incorrectly named?

nimmolo
  • 191
  • 3
  • 14

1 Answers1

5

Let me first explain the log messages.

Zeitwerk scans the project, and found a directory called queued_email before finding queued_email.rb. So, as a working hypothesis it assumed QueuedEmail was an implicit namespace with the information that it had. This hypothesis got later invalidated when it saw queued_email.rb, and said "wait, this is actually an explicit namespace". So it undid the implicit setup, and redefined it to load an explicit namespace.

Now, let's go for the subclasses.

When an application does not eager load, files are only loaded on demand. For example, if you load QueuedEmail, and app/models/queued_email has 24 files recursively, none of them are loaded until they are used.

When a class is subclassed, the collection returned by subclasses is populated. But you don't know a class is subclassed until the subclass is loaded. Therefore, in a lazy loading environment subclasses is empty at the start. If you load 1 subclass it will have that one, but not the rest, until they are all eventually loaded.

If you need the subclasses to be there for the application to function properly, starting with Zeitwerk 2.6.2 you can throw this to an initializer

# config/initializers/eager_load_queued_email.rb
Rails.application.config.to_preprare do
  Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/queued_email")
end
Xavier Noria
  • 1,640
  • 1
  • 8
  • 10
  • Thank you for explaining this so clearly. Is there any reason not to eager-load these classes? It doesn't seem so, but just asking. The reason i want them available is the parent model makes an array of the subclasses available to controllers. – nimmolo Dec 08 '22 at 06:13
  • Just updated to Zeitwerk 2.6.6. But I think maybe there's an error in the initializer syntax you quoted. `to_prepare` is one, but I'm not finding the method `eager_load_dir` for `autoloaders.main` – nimmolo Dec 08 '22 at 06:29
  • Since that initializer doesn't work for me yet, I added the path to the subclasses to the `config.eager_load_paths`, in `config/application.rb`. But I also have `config.eager_load = false` in my `config/environments/development.rb`. Is that maybe overriding whatever I'm doing with `eager_load_paths`? – nimmolo Dec 08 '22 at 07:37
  • If you need to eager load these classes you can do so, no prob. The to_prepare block should work. Which error are you getting with the initializer I suggested? – Xavier Noria Dec 08 '22 at 07:52
  • Error says `eager_load_dir` is not a method. I realized, of course, environment config for development (eager_load = false) does override application config. Another question is, with Zeitwerk are there good reasons not to turn `eager_load` on in development? We have it off, in this older app, but that config predates zeitwerk. – nimmolo Dec 08 '22 at 07:57
  • It is customary to not eager load the entire app in development for performance. However, if that is not an issue in yours, just flip config.eager_load for development and test. That said, the initializer should work, that method is available since 2.6.2. – Xavier Noria Dec 08 '22 at 07:58
  • The exact error was `undefined method 'eager_load_dir' for #` – nimmolo Dec 08 '22 at 08:00
  • You using Spring perhaps? – Xavier Noria Dec 08 '22 at 08:03
  • Won't have access to a computer till late a night. If the loader does not respond to that mehod, for some reason either Gemfile.lock is not updated, or the app not properly restarted. I'll check the thread later! – Xavier Noria Dec 08 '22 at 08:12