Defect #36245

ActiveSupport::Reloader.to_prepare not working in trunk 21287

Added by Alexander Meindl 10 months ago. Updated 9 months ago.

Status:ResolvedStart date:
Priority:NormalDue date:
Assignee:-% Done:

0%

Category:Plugin API
Target version:-
Resolution: Affected version:

Description

With r21283 in trunk ActiveSupport::Reloader.to_prepare and Rails.configuration.to_prepare is not fired anymore.

ActiveSupport::Reloader.to_prepare is required for plugins (as an example), if some code should loaded after all plugins (other plugins) are loaded.

Maybe there is another possibility with Rails 6 or Zeitwerk for doing this, but imho ActiveSupport::Reloader.to_prepare should work within plugins, too. See https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoloading-when-the-application-boots


Related issues

Related to Redmine - Patch #34072: Hook after plugins were loaded Closed
Related to Redmine - Feature #32938: Rails 6: Zeitwerk support Closed

History

#1 Updated by Alexander Meindl 10 months ago

The defect is for the usage of ActiveSupport::Reloader.to_prepare in a plugin - not in Redmine itself (just to make it clear).

I did not find a solution to fix this behavior until now.

Here is an example for a plugin init.rb:

Redmine::Plugin.register :my_plugin do
  name 'My plugin'
  version '0.01'
end

ActiveSupport::Reloader.to_prepare do
  raise 'this is never called'
end

Rails.configuration.to_prepare do
  raise 'this is never called, too'
end

#2 Updated by Takashi Kato 10 months ago

I apologize for the delay in responding.
Before the introduction of zeitwerk, the autoloader loads Redmine plugins on initializing.

After the introduction of zeitwerk, to make the Redmine::Plugin class manageable for zeitwerk, Redmine::PluginLoader runs the "init.rb" for all plugins inside the "Rails.configuration.to_prepare" block (and run on every reload).

https://www.redmine.org/projects/redmine/repository/entry/trunk/lib/redmine/plugin_loader.rb#L108

Now processing written inside the "Rails.configuration.to_prepare" block in "init.rb" can be written directly in "init.rb".
Let me know if there are any problems in creating plugins.

#3 Updated by Takashi Kato 10 months ago

Alexander
It may not be a plugin you want to fix, but I found your plugin on GitHub and made it compatible with zeitwerk.

https://github.com/tohosaku/redmine_emojibutton/commits/zeitwerk

#4 Updated by Alexander Meindl 10 months ago

Hi Takashi,

thanks for your answer. The problem is, you cannot use Classes from Plugin B with Plugin A - because Plugin B is not initialized at this moment. Because of this, till now without zeitwerk, the solution was to use this Classes after all plugins are initialized (with Rails.configuration.to_prepare).

If Redmine::PluginLoader loads all plugins in "Rails.configuration.to_prepare" block, it is not possible to call an "Rails.configuration.to_prepare" block in a plugin again. This would be a "Rails.configuration.to_prepare" block in a "Rails.configuration.to_prepare" block - and this does not work - as it looks at the moment.

Here are some examples: https://github.com/AlphaNodes/additional_tags/blob/master/init.rb or https://github.com/AlphaNodes/redmine_saml/blob/master/init.rb or https://github.com/AlphaNodes/redmine_sudo/blob/master/init.rb (we build an plugin loader for that)

I am not sure, if you get me right. I try to explain, that if you have dependencies between plugins (which we have a lot), there is no way (or I do not know it), how we can run code from a plugin, after all plugins are initialized.

An example: Plugin B requires Plugin C. You cannot use Plugin C code in Plugin B, till it is initialized - and this worked perfectly with Rails.configuration.to_prepare before zeitwerk. Maybe to provide a hook after initializing all plugins could be a solution.

#5 Updated by Ko Nagase 10 months ago

Hi Alexander,

I still haven't tried Redmine trunk yet, but I encountered the similar situation which needs to control plugins load orders in Redmine 4.2-stable branch.
https://github.com/gtt-project/redmine_gtt/pull/130

From glance of Takashi's comment,

https://www.redmine.org/projects/redmine/repository/entry/trunk/lib/redmine/plugin_loader.rb#L108

I noticed that there seems to be after_plugins_loaded hook which seems to be called when all plugins are loaded, so I guess that we can try to use this way as a workaround.
https://www.redmine.org/issues/20263

#6 Updated by Alexander Meindl 10 months ago

Hi Ko,

indeed I found after_plugins_loaded hook some hours ago. But the problem with that is, you cannot use a patched method in Redmine::Plugin.register block (e.g. to add a link to menu for special conditions, which requires a patch, which is applied later with after_plugins_loaded hook).

Maybe the way with after_plugins_loaded hook is the right direction. Not sure, if there are more problems with that.
But dispense with Rails.configuration.to_prepare means a lot of rework/adjustments in plugins.

#7 Updated by Alexander Meindl 10 months ago

  • Status changed from New to Resolved

after_plugins_loaded hook works for me as a replacement for Rails.configuration.to_prepare

#8 Updated by Go MAEDA 10 months ago

  • Related to Patch #34072: Hook after plugins were loaded added

#9 Updated by Go MAEDA 10 months ago

#10 Updated by Takashi Kato 10 months ago

Hi Alexander,

Glad that this is solved!
Thank you for your very meaningful report as it was a case that I hadn't really anticipated.

#11 Updated by Ko Nagase 9 months ago

Sorry for the very late reply.

I tried to check Zeitwerk plugin load sequence by "puts" debug on the latest 4.2-stable and master (trunk) branches with using ruby 2.7.4, and the difference was as follows:
4.2-stable master (trunk)
code
puts 'MyPlugin - init.rb'

Redmine::Plugin.register :my_plugin do
  puts 'MyPlugin - Redmine::Plugin.register'
  name 'My Plugin plugin'
  author 'Author name'
  description 'This is a plugin for Redmine'
  version '0.0.1'
  url 'http://example.com/path/to/plugin'
  author_url 'http://example.com/about'
end

ActiveSupport::Reloader.to_prepare do
  puts 'MyPlugin - ActiveSupport::Reloader.to_prepare'
end

Rails.configuration.to_prepare do
  puts 'MyPlugin - Rails.configuration.to_prepare'
end

Rails.application.config.to_prepare do
  puts 'MyPlugin - Rails.application.config.to_prepare'
end

Rails.application.reloader.to_prepare do
  puts 'MyPlugin - Rails.application.reloader.to_prepare'
end

Rails.application.config.after_initialize do
  puts 'MyPlugin - Rails.application.config.after_initialize'
end

class AfterPluginsLoadedHook < Redmine::Hook::Listener
  def after_plugins_loaded(context = {})
    puts 'MyPlugin - after_plugins_loaded hook'
  end
end
puts 'MyPlugin - init.rb'

Redmine::Plugin.register :my_plugin do
  puts 'MyPlugin - Redmine::Plugin.register'
  name 'My Plugin plugin'
  author 'Author name'
  description 'This is a plugin for Redmine'
  version '0.0.1'
  url 'http://example.com/path/to/plugin'
  author_url 'http://example.com/about'
end

ActiveSupport::Reloader.to_prepare do
  puts 'MyPlugin - ActiveSupport::Reloader.to_prepare'
end

Rails.configuration.to_prepare do
  puts 'MyPlugin - Rails.configuration.to_prepare'
end

Rails.application.config.to_prepare do
  puts 'MyPlugin - Rails.application.config.to_prepare'
end

Rails.application.reloader.to_prepare do
  puts 'MyPlugin - Rails.application.reloader.to_prepare'
end

Rails.application.config.after_initialize do
  puts 'MyPlugin - Rails.application.config.after_initialize'
end

#class AfterPluginsLoadedHook < Redmine::Hook::Listener
Class.new(Redmine::Hook::ViewListener) do |c|
  def after_plugins_loaded(context = {})
    puts 'MyPlugin - after_plugins_loaded hook'
  end
end
result
# Init server
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - after_plugins_loaded hook
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
MyPlugin - Rails.configuration.to_prepare
MyPlugin - Rails.application.config.to_prepare
MyPlugin - Rails.application.config.after_initialize

# Reload 1st after editing "plugins/my_plugin/config/locales/en.yml" 
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
MyPlugin - Rails.configuration.to_prepare
MyPlugin - Rails.application.config.to_prepare

# Reload 2nd after editing "plugins/my_plugin/config/locales/en.yml", again
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
MyPlugin - Rails.configuration.to_prepare
MyPlugin - Rails.application.config.to_prepare
# Init server
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - after_plugins_loaded hook
MyPlugin - Rails.application.config.after_initialize

# Reload 1st after editing "plugins/my_plugin/config/locales/en.yml" 
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - Rails.application.config.after_initialize
MyPlugin - after_plugins_loaded hook
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare

# Reload 2nd after editing "plugins/my_plugin/config/locales/en.yml", again
MyPlugin - init.rb
MyPlugin - Redmine::Plugin.register
MyPlugin - Rails.application.config.after_initialize
MyPlugin - after_plugins_loaded hook
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare
MyPlugin - ActiveSupport::Reloader.to_prepare
MyPlugin - Rails.application.reloader.to_prepare

In master (trunk) branch, I had to change the Hook class definition, because of the following error when reloading.
(Thanks tohosaku and @matobaa for the @redmine_ld_rize plugin's commit!)

TypeError (superclass mismatch for class AfterPluginsLoadedHook):

plugins/my_plugin/init.rb:29:in `<top (required)>'
lib/redmine/plugin_loader.rb:31:in `load'
lib/redmine/plugin_loader.rb:31:in `run_initializer'
lib/redmine/plugin_loader.rb:108:in `each'
lib/redmine/plugin_loader.rb:108:in `block in load'

In master (trunk) branch,
  • "ActiveSupport::Reloader.to_prepare" and "Rails.application.reloader.to_prepare" are actually called when reloading, but not called at initialization.
  • "ActiveSupport::Reloader.to_prepare" and "Rails.application.reloader.to_prepare" seem to be called multiple times after 2nd reloading, and I think that this behavior needs to be fixed.
  • "after_plugins_loaded" hook called timing is different between 4.2-stable branch (only once) and master (trunk) (every load/reload), and I think that same called timing is preferable (especially when supporting both Redmine 4.2 and 5.0 in the plugin).

2022-04-04: Added "Rails.application.config.after_initialize" in above table

#12 Updated by Ko Nagase 9 months ago

"after_plugins_loaded" hook called timing is different between 4.2-stable branch (only once) and master (trunk) (every load/reload), and I think that same called timing is preferable (especially when supporting both Redmine 4.2 and 5.0 in the plugin).

Well, about this, just separating the event handler to "Rails.application.config.after_initialize" seems to be enough.

Also, in current master (trunk), "Rails.application.config.after_initialize" event handler (in the plugin's "init.rb") seems to be called at every load/reload timing without duplicate call, so this can be also used instead of other ".to_prepare" functions of Redmine <= 4.2. (But I am not sure whether this is expected result...)

Here is the combination result from above diffs.


FYI

#13 Updated by Ko Nagase 9 months ago

"ActiveSupport::Reloader.to_prepare" and "Rails.application.reloader.to_prepare" seem to be called multiple times after 2nd reloading, and I think that this behavior needs to be fixed.

About this, just adding once execution guard by class variable may be enough.

With this change, the result becomes as follows, and I think that this is the most similar with past (Redmine <= 4.2) sequence.

#14 Updated by Ko Nagase 9 months ago

Sorry, above note-13 comment seemed to be completely wrong...

Now, I am using "Rails.application.config.after_initialize" with current master (trunk) branch, and it seems to be no problem.
https://github.com/gtt-project/redmine_custom_fields_groups/pull/14

Also available in: Atom PDF