Max Ivak
Max Ivak

Reputation: 1549

Rails 6 Zeitwerk unloads my class after initializer

I am implementing a singleton class/module in Rails 6 application using Zeitwerk loader.

# app/lib/mynamespace/mymodel.rb

module Mynamespace
  module Mymodel
    class << self
      attr_accessor :client
    end

    def self.client
      @client ||= "default_value"
    end

    def self.client=(client)
      @client = client
    end
end

Singleton class is initialized in

# config/initializers/mymodel.rb

Mynamespace::Mymodel.client = "my_custom_value"
# Mynamespace::Mymodel.client - this returns correct value

Then when I use the singleton class in a controller

# app/controllers/mycontroller.rb

client = Mynamespace::Mymodel.client

it returns an empty object as it was not initialized: client == "default_value" but should be "my_custom_value".

Log shows errors

DEPRECATION WARNING: Initialization autoloaded the constants Mynamespace::Mymodel

Autoloading during initialization is going to be an error condition in future versions of Rails.

How to properly configure a singleton class while using Zeitwerk ?

Upvotes: 1

Views: 2736

Answers (2)

Unixmonkey
Unixmonkey

Reputation: 18784

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.

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

module MyApp
  class Application < Rails::Application
    # other stuff
    config.after_initialize do
      Mynamespace::MyModel.client = "my_custom_value"
    end
  end
end

Upvotes: 2

Xavier Noria
Xavier Noria

Reputation: 2324

This is why referring reloadable constants has been finally forbidden in Rails 7, because it doesn't make sense and you find the hard way.

This is unrelated to Zeitwerk, it is related to logic about reloading itself.

TLDR: Since code in app/lib is reloadable (that is why you put it there), you need to consider that on reload the initialization has to happen again. That is accomplished with a to_prepare block. Please have a look at https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoloading-when-the-application-boots.

On the other hand, if you are good about not reloading that singleton, then you can move it to the top-level lib and issue a require for it in the initializers. (Assuming that lib is not in the autoload paths, which is not by default.)

Upvotes: 1

Related Questions