1

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 ?

Max Ivak
  • 1,529
  • 17
  • 39

2 Answers2

1

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.

Unixmonkey
  • 18,485
  • 7
  • 55
  • 78
  • after moving the initialization code to `config/application.rb` it gives an error `uninitialized constant Mynamespace::Mymodel` and doesn't boot. – Max Ivak Apr 09 '20 at 23:53
  • it works if placing my code in `after_initialize` block in `config/application.rb`: `config.after_initialize do Mynamespace::Mymodel.client = "my_custom_value" end`. Add an example to your answer and i will accept it. – Max Ivak Apr 10 '20 at 00:04
  • Zeitwerk will reload my class after changes in development mode and the singleton class won't work - value stored in `Mynamespace::Mymodel.client` is reset to nil after reload. – Max Ivak Apr 10 '20 at 01:22
  • Regarding the uninitialized constant, that means you should `require` the file that holds the module definition. What do you mean by the last comment? Are you editing the code in MyModel, and expecting it to hold the value set during initialization? – Unixmonkey Apr 10 '20 at 01:53
  • i am editing the code in app's class which uses Mymodel `class AnotherClass def bar client = Mynamespace.Mymodel.client end end`. if I edit `AnotherClass` with any new code in class, Zeitwerk unloads initialized Mymodel and `Mymodel.client` returns nil. – Max Ivak Apr 10 '20 at 05:49
  • I wonder if this might help: https://stackoverflow.com/a/39498710/23915 Basically, remove `MyModel` from somewhere Rails autoloads (take it out of `/app` and put in a top-level `/lib` dir or something like that, or remove it from the autoload paths with something like https://stackoverflow.com/a/59422729/23915 shows. – Unixmonkey Apr 10 '20 at 16:10
  • 1
    Let me say that Zeitwerk does not load the gems in the Gemfile (Bundler does), and does not run initializers either (Rails does manually). Zeitwerk is only responsible for _autoloadable_ code in your application (think what's under `app`), and its engine dependencies (what's under their `app`s). – Xavier Noria Feb 17 '22 at 22:05
0

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.)

Xavier Noria
  • 1,640
  • 1
  • 8
  • 10