rails: Unexpected deprecation warning in Rails 6: 'Initialization autoloaded the constant Example'

Steps to reproduce

  1. Create a new app with Rails 6.0.0.rc1 (or tip of 6-0-stable branch)
  2. The app initializes fine. Confirm with rails test.
  3. Define a simple ActiveModel::Type
# app/types/example.rb
class Example < ActiveModel::Type::String
end

# config/initializers/types.rb
ActiveRecord::Type.register(:example, Example)
  1. Now the app initializes with deprecation warnings. Confirm with rails test.

Expected behavior

No Rails deprecation warnings. This is a bare app, using a simple Rails feature as documented.

Actual behavior

$ rails test
DEPRECATION WARNING: Initialization autoloaded the constant Example.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload Example, for example,
the expected changes won't be reflected in that stale Class object.

This autoloaded constant has been unloaded.

Please, check the "Autoloading and Reloading Constants" guide for solutions.
 (called from <top (required)> at /Users/ahoyt/code/tiu/testApp/config/environment.rb:5)

System configuration

Rails version: 6.0.0.rc1 (or with tip of 6-0-stable branch)

Ruby version: 2.5.5

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 24
  • Comments: 22 (16 by maintainers)

Commits related to this issue

Most upvoted comments

Hi!

My understanding of this problem is that when the application encounter ActiveRecord::Type.register(:example, Example) in your initializer, this is the first time Example is referenced, so Rails will try to autoload it. As the deprecation warning message says, Autoloading during initialization is not good because it will not reload correctly.

Wrapping your initializer with ActiveSupport.on_load(:active_record) will wait until ActiveRecord has finished loading. By now, your file example.rb would have been autoloaded as part of the app folder being loaded. When the content of the initializer is executed, it will not be the first reference to Example, so rails won’t have to try to autoload it. This also has the advantage of preventing an early load of ActiveRecord.

In your config/initializers/types.rb:

ActiveSupport.on_load(:active_record) do
  ActiveRecord::Type.register(:example, Example)
end

@fxn or somebody more knowledgeable in Rails Autoloading could probably give you a more authoritative answer.

Ah, of course, that makes sense! Thanks @fxn for the explanation!

I forgot that any app/* paths are autoloaded! So, I introduced this autoloaded path into the types initializer…and fully deserved the warning.

$ spring stop
$ rails r 'puts ActiveSupport::Dependencies.autoload_paths'
...
/Users/ahoyt/code/testApp/app/types

The deprecation warning about autoloaded constants went away when I followed your suggestion. I moved my types to lib and simply required them directly:

# lib/types/example.rb
class Example < ActiveModel::Type::String
end

# config/initializers/types.rb
Dir[Rails.root.join('lib', 'types', '**', '*.rb')].each { |f| require f }
ActiveRecord::Type.register(:example, Example)

Now, all my types work, my tests pass, and neither app/types or lib/types are in my autoload_paths.

Happy dance! 😀 🎉 🕺 🥂

@jackkinsella could you please try this?

  1. Define CurrencyType as you did before, without the line that registers.

  2. In an initializer register in a to_prepare block:

    Rails.application.config.to_prepare do
      ActiveRecord::Type.register(:currency, CurrencyType)
    end
    

If register overwrites the previous :currency entry as I believe, that should do the trick and the class would be reloadable.

Not quite. Let me explain.

require_dependency should not be used, that helper is related to autoloading in classic mode, and it keeps track of reloadable constants. Applications running in zeitwerk mode should not use require_dependency.

What you want, is to load the type, register it, and done. You do not want the class object stored in the constant to become stale if you edit the file and reload. You want the class object to be valid for the entire lifetime of the process regardless of reloads.

So, you have lib/types/example.rb, and in the initializer you

require 'types/example'

which is going to work because lib belongs to $LOAD_PATH.

Most of the Ruby action in a Rails app happens in the app folder and it’s all happily autoloaded, making for a wonderful development experience where you can carry out continuous experiments in development without needing to restart the server. Why do ActiveModel::Type subclasses need special treatment? Is it performance related, due to load order constraints, or “that’s just the way it is”?

This is not something related to Zeitwerk.

The type is registered in an initializer. No matter whether you use classic or zeitwerk mode, if you edit your custom type and reload, changes won’t take effect because initializers run only when the application boots.

So before Rails 6, you could think the edit had effect, but it did not. Advising against this is the point of the warning in Rails 6.

I believe it should be possible to get it working if you registered at the bottom of the file that implements the custom type (because I believe the register would override the entry with the same key), and configured a callback to force loading that constant. But I don’t have time right now to test that idea.

Hey @jasonperrone, could you please read sections 6.1 and 6.2 in the edge autoloading guide, and see if that solves your use case?

If you edit app/types/example.rb, the registered class object will become stale, won’t reflect the changes. One way to do this cleanly is to move the types directory to lib, and perform a regular require in the initializer.

For @ansonhoyt , I think he just needs require ‘example’ since files in “app/types/” should already be autoloaded.

Not really. It would be confusing and a potential issue to have a directory in autoload_paths for files that do not have to be autoloaded nor reloaded.

As I said above, better move that to lib.

There is a Rails Guide, Autoloading and Reloading Constants (Zeitwerk Mode). Check out section 6.1 Reloading and Stale Objects.

The docs for the attribute method are pretty good for how to work with types, but it doesn’t warn about the pitfalls of how you load your custom types. That would make a good addition!

To fix this exception, just make sure you don’t autoload your custom types, like I did, but put them in /lib. See comment above for how I load my custom types.

btw, the above advice from @fxn is solid. He wrote zeitwerk.

Thanks @fxn for helping to sort that out. Might suggest adding something about requiring deps in initializers in the autoloading section of the upgrade guide.

For @ansonhoyt , I think he just needs require 'example' since files in “app/types/” should already be autoloaded.