loading...

Replace A Rails Initializer

st0012 profile image Stan Lo ・3 min read

Rails uses a series of initializers to initialize your application. While some of them might come from the gems you use, most of them are the Rails' built-in ones. And in some rare cases, you might need to customize some of them for special usages.

For example, I had to replace my app's set_routes_reloader_hook initializer (the one that generates routes for your application) to benchmark our route generation logic. And it took a while to figure out how to do it the right way.

So if you don't need to do this, that's totally normal. But if you do, this post is for you.

The Steps

In this post, I'll use the set_routes_reloader_hook initializer as our example. And the replacement will be done in 3 steps:

  1. Identify the Initializer's Source
  2. Register the New Initializer
  3. Drop the Old Initializer

And finally, I'll show you how to reuse the original initializer's body instead of copy&paste the code.

Identify the Initializer's Source

When talking about replacing a Rails initializer, your first thought might be modifying the Rails::Application#initializers directly. But that won't work.

In Rails, Rails::Application#initializers actually consists of 3 initializer sources:

def initializers #:nodoc:
  Bootstrap.initializers_for(self) +
  railties_initializers(super) +
  Finisher.initializers_for(self)
end

So every time you call Rails::Application#initializers, it returns a new array of initializers, and the change made on it won't be applied to the rest of the app.

What we need to do instead, is to identify which source that our target initializer belongs to:

Rails::Application::Bootstrap.initializers_for(Rails.application).map(&:name).include?(:set_routes_reloader_hook) #=> false
Rails::Application.initializers_for(Rails.application).map(&:name).include?(:set_routes_reloader_hook) #=> false
Rails::Application::Finisher.initializers_for(Rails.application).map(&:name).include?(:set_routes_reloader_hook) #=> true

Now we can see that the set_routes_reloader_hook is defined in Rails::Application::Finisher.

Register the New Initializer

Because Rails executes initializers in the order they are defined, we need to make sure the new initializer is placed in the right place. So we need to use the target initializer as our index before dropping it. To do this, we can use the before or after argument when registering the new initializer:

# config/application.rb

require 'rails/all'

Rails::Application::Finisher.initializer :set_routes_reloader_hook_with_benchmark, after: :set_routes_reloader_hook do |app|
  # ....
end

Drop the Old Initializer

And then the last step: drop the target initializer

# config/application.rb

require 'rails/all'

Rails::Application::Finisher.initializer :set_routes_reloader_hook_with_benchmark, after: :set_routes_reloader_hook do |app|
  # ....
end

Rails::Application::Finisher.initializers.reject! { |i| i.name == :set_routes_reloader_hook }

Additional Tip: Reuse the Old Initializer

Since we're "replacing" the initializer instead of just dropping it or adding a new one, it means the new initializer may be very similar to the old one or just an extension of it. But because initializers aren't methods, we can't use super like

def set_routes_reloader_hook
  super
  # other stuff you want to do
end

If this is bothering you, here's a trick: We can extract the old initializer's block, and use instance_eval to evaluate it inside the new initializer


original_initializer_block = Rails::Application::Finisher.initializers.detect { |i| i.name == target_initializer }.block

Rails::Application::Finisher.initializer :new_initializer, after: target_initializer do |app|
  instance_eval(&original_initializer_block)
  # other stuff you want to do
end

Rails::Application::Finisher.initializers.reject! { |i| i.name == target_initializer }

Thank you for reading this post. If you like it, I also wrote some articles about debugging Rails applications/Ruby programs

I'm also working on a gem called tapping_device. It can help you debug Ruby programs more easily by making objects tell you what they do.
Not sure what it means? There are examples in the readme 👇

GitHub logo st0012 / tapping_device

TappingDevice makes the objects tell you what they do, so you don't need to track them yourself.

Posted on May 31 by:

st0012 profile

Stan Lo

@st0012

Senior Software Engineer @ticketsolve. Love Cats, Ruby on Rails and Boxing. Created Goby. Working on tapping_device during weekends.

Discussion

markdown guide
 

what is the motivation of doing so? I don't see a clear advantage.

 

Hi thanks for the comment! I have just updated post intro accordingly!

My use case was to benchmark our app's route generation, so I had to replace the initializer to insert some benchmark logic.

But this post is not encouraging people to do so, just to provide a way to those who need to do it. Because it took me a while to do it right 😬