Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I keep my initializer configuration from being lost in development mode?

I'm working on a Rails app that uses an engine. I'm using an initializer to configure one of my engine's controllers so that it will trigger an action in the host app. The code looks something like this:

# config/initializers/my_engine.rb
MyEngine::SomeController.after_filter proc {
  # Do something in the host app
}, :only => :update

This works fine in production, but in development mode, the proc is only called on the first request. This is because the classes are getting reloaded and this configuration is lost, because it was stored in a class variable. (For example, MyEngine::SomeController is reloaded from the file it's in, and since the after_filter isn't declared there, it isn't added back on.)

Some Rails background

In development mode, Rails uses the following load strategy:

  • Code in the app directory is reloaded on each request, on the assumption that you're actively changing it.
  • Code in the lib directory, along with config/initializer files, are loaded once, when the application boots.

Initializer files are generally used for configuring gems. In the past, gems have mostly had code in the lib directory, so running their configuration once was sufficient.

How engines change things

However, Rails engines have code in the app directory: controllers, models, etc. These files are reloaded in development mode on each request. Therefore, configuration like my example above is lost.

Enter to_prepare

Rails provides config.to_prepare specifically to solve this problem: it run once in production, and on every request in development.

For example, we have this in application.rb, which works fine:

config.to_prepare do
  # set up class variables (after_filters, etc)
end

However, if I have to put all my engines' configuration in application.rb, this defeats the point of config/initializers in keeping things organized.

So, for any configuration of classes in my engines' app directories, I want to put that code in files under config/initializers.

Here are my questions.

  • I'm unclear how to get config into scope in an initializer file. I'm thinking it would be Rails.application.config. Is that right?
  • Can I add add multiple to_prepare blocks? I'm afraid that calling it multiple times will overwrite previous blocks.

Update

As @Frederick Cheung mentioned, Rails.application.config.to_prepare does work in config/initializer files, and one can use as many of these as needed in the various files; each call appends its block to an array, so nothing is overwritten.

So the solution to this problem is:

# config/initializers/my_engine.rb
Rails.application.config.to_prepare do
  MyEngine::SomeController.after_filter proc {
    # Do something in the host app
  }, :only => :update
end

One thing that still seems odd: I expected the to_prepare block to be called on every request in development mode, but instead it seems to be called randomly every 3rd request or so. I added block:

Rails.application.config.to_prepare do
  Rails.logger.info "Running the prepare block!"
end

... restarted my app, and refreshed the page nine times. I only saw the message on the 1st, 5th, 7th and 9th requests. I'm not sure what explains this behavior, but it does explain why my code without the to_prepare worked intermittently in development.

like image 365
Nathan Long Avatar asked Jan 17 '12 13:01

Nathan Long


2 Answers

You can add as many to_prepare blocks as you want - when you do config.to_prepare, Rails is doing (in configuration.rb in railties)

def to_prepare(&blk)
  to_prepare_blocks << blk if blk
end

and then iterates over those blocks handing them over to ActionDispatch::Reloader, where to_prepare is implemented using ActiveSupport::Callbacks (i.e. the same thing that is used for before_save and so on). Multiple to_prepare blocks are fine.

Currently it looks like Rails iterates over to_prepare_blocks after reading application initialisers so adding to Rails.application.configuration.to_prepare should work. You may prefer to use ActionDispatch::Reloader.to_prepare.

like image 180
Frederick Cheung Avatar answered Oct 19 '22 09:10

Frederick Cheung


There's nothing to stop you from doing initializer code in a file that lives in app/models.

for example

class MyClass
  def self.run_me_when_the_class_is_loaded
  end
end

MyClass.run_me_when_the_class_is_loaded

MyClass.run_me... will run when the class is loaded .... which is what we want, right?

Not sure if its the Rails way.... but its extremely straightforward, and does not depend on the shifting winds of Rails.

like image 38
Bret Weinraub Avatar answered Oct 19 '22 11:10

Bret Weinraub