Zeitwerk overwriting values set in initializers in Rails 6

Hi,

Ran into something interesting while trying to use ActiveSupport::Configurable in Rails 6. It appears the OrderedHash created by ActiveSupport::Configurable is being overwritten in my development environment only.

I have a module Foo with a class Bar as follows:

module Foo
  class Bar
    include ActiveSupport::Configurable

    def self.request_body
      { 
        'client_id' => my_client_id,
        'client_secret' => my_client_secret
      }
    end
  end
end

and its corresponding initializer

Foo::Bar.configure do |c|
  if Rails.env.production? || Rails.env.staging?
    %i[my_client_id my_client_secret].each do |key|
      # set for prod and staging using Rails credentials
    end
  elsif Rails.env.test?
    %i[my_client_id my_client_secret].each do |key|
      # set for test
    end
  else
    %i[my_client_id my_client_secret].each do |key|
      c[key] = '1234'
    end
  end
end

If I insert byebug at the end of my initializer, start the rails console and call

Foo::Bar.config #=> {:my_client_id=>"1234", :my_client_secret=>"1234"}

But when I exit byebug and reach the console prompt and run the above I get an empty hash.

I have found two “workaround” solutions. In development.rb, setting config.cache_classes to true allows the OrderedHash to be set as expected. Likewise, reverting to the classic autoloader in application.rb with config.autoloader = :classic produces the expected outcome.

I have read the docs at Autoloading and Reloading Constants (Zeitwerk Mode) — Ruby on Rails Guides but can’t quite figure out what is happening here.

Any insight would be appreciated.

Thank you

Hi! Do you see a warning in the logs when the application boots?

No warnings. Running rails s produces the typical:

=> Booting Puma
=> Rails 6.0.3.3 application starting in development 
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 5.0.2 (ruby 2.5.1-p57), codename: Spoony Bard
* Min threads: 5, max threads: 5
* Environment: development
* Listening on http://0.0.0.0:3000
Use Ctrl-C to stop

Thank you

Where’s foo/bar.rb located? Is Foo::Bar autoloaded?

I have several modules and it requires some api keys. For exmaple bp credit card login

MyModule.configure do |config|
  config.api_key = '...'
end 
And all of sudden, it is not working only in development. It seems that it does not load this configure block as initially so my module does not know what the keys are. Any idea?

Every time I encounter values not being set from an initializer, I blame Spring. Try using spring stop in your project folder, and see if that wakes things up.

Walter

I believe the issue here is that the way Zeitwerk loads your code, it’s first loading Gems from your Gemfile, then running initializers, then loading your application code, so trying to run Mynamespace::MyModel.client , means it has to stop what it’s doing and load app/lib/mynamespace/mymodel.rb to load that constant, to execute client= on it.

This also means that if you change the Mynamespace::MyModel code, Rails will not be able to hot-reload the constant, because initializers don’t get re-run, introducing a circular dependency lock (have you ever seen an error like “module MyModel removed from tree but still active!” or have to use require_dependency before using some code that should be autoloaded but isn’t?). Zeitwerk attempts to fix that class of issues Kroger Feed

Move that code out of config/initializers , and into config/application.rb , and it will still be run on boot.

If MyModule is autoloaded, the problem is that this module will be redefined when a reload happens. Initializers only run when the application boots, so nobody is setting the API key again on reload. Let me break it down:

  1. Application boots, the initializer is executed, MyModule autoloaded, and the API key set.
  2. A reload happens.
  3. MyModule is reloaded, but the initializer does not run again, therefore the fresh new module does not have an API key set.

This is not specific to Zeitwerk, it has been this way always.

However, in Rails 6 we make that more apparent so that you are aware of the latent issue, and a verbose warning is issued. I have in mind to make autoloading during initialization an error condition in the future, because this gotcha is very common.

Please check these docs for a way to do that correctly. Even easier, if you upgrade Zeitwerk to the last version you can write that initializer this way:

Rails.autoloaders.main.on_load("MyModule") do
  MyModule.configure do |config|
    config.api_key = '...'
  end 
end

Maybe there is going to be API for that in the future, but for Rails 6.1 it is better not to force the last version of the library as a gem dependency.

1 Like

Xavier, your suggestion works great for me. Thanks for taking the time to explain. I appreciate it.

1 Like