2017-08-04

Railtieのinitializerが読み込まれる仕組み

Railtieのサブクラスで呼び出したinitializerブロックが読み込まれる仕組みをコードを読んで紐解いてみました。Railsは5.1.2です

Railtieでのinitializer呼び出し

initializerメソッドはRails::Initializableモジュールに定義されています。Rails::Initializable.initializerメソッドではInitializerのインスタンスを作ってCollectionに追加します。opts[:after]の行はTSortによるソート用に定義しており、基本的にはinitializerが呼び出された順にブロックの処理が走るようになっています。
require "tsort"

module Rails
  module Initializable
    module ClassMethods
...
      def initializers
        @initializers ||= Collection.new
      end
...
      def initializer(name, opts = {}, &blk)
        raise ArgumentError, "A block must be passed when defining an initializer" unless blk
        opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] }
        initializers << Initializer.new(name, nil, opts, &blk)
      end
...

これらはクラスメソッドなので各Railtieのクラスインスタンス変数に@initializersとしてInitializerのCollectionが格納されることになります。

これらの呼び出しはRails::Applicationから行います。

config/environment.rbで <span class="s1">Rails</span>.application.initialize! を呼びだします

module Rails
  class Application < Engine
...
    # Initialize the application passing the given group. By default, the
    # group is :default
    def initialize!(group = :default) #:nodoc:
      raise "Application has been already initialized." if @initialized
      run_initializers(group, self)
      @initialized = true
      self
    end
...

Rails::Initializable#run_initializersは自身のinitializersをTSortでトポロジカルソートしたものを順にrunしていきます。

module Rails
  module Initializable
...
    def run_initializers(group = :default, *args)
      return if instance_variable_defined?(:@ran)
      initializers.tsort_each do |initializer|
        initializer.run(*args) if initializer.belongs_to?(group)
      end
      @ran = true
    end
...

Initializer#runはRails::Initializable.initializerで定義したブロックを実行するメソッドです。

module Rails
  module Initializable
    class Initializer
...
      def run(*args)
        @context.instance_exec(*args, &block)
      end
...

Rails::Initializableに#initializersが定義されているのですが、Rails::Applicationにも#initializersが定義されているので、こちらが呼び出されます。

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

railties_initializersはordered_railtiesのinitializersの集合になります。

def railties_initializers(current) #:nodoc:
  initializers = []
  ordered_railties.reverse.flatten.each do |r|
    if r == self
      initializers += current
    else
      initializers += r.initializers
    end
  end
  initializers
end

ordered_railtiesにはrailtiesの中身が入り、railtiesはRailtiesのインスタンスです。Rails::Engine::Railties#eachメソッドで::Rails::Railtieのサブクラス、::Rails::Engineのサブクラスのインスタンスがセットされます。これによってRailtieを継承したクラスのinitializerで渡したブロックが順に実行されることになります。

module Rails
  class Engine < Railtie
    class Railties
      include Enumerable
      attr_reader :_all

      def initialize
        @_all ||= ::Rails::Railtie.subclasses.map(&:instance) +
          ::Rails::Engine.subclasses.map(&:instance)
      end

      def each(*args, &block)
        _all.each(*args, &block)
      end

      def -(others)
        _all - others
      end
    end
  end
end

TSortによるトポロジカルソート

さて、Railtieでは以下のようにbefore, afterキーワード引数によってinitializerの実行順の制御をすることができました。
module HttpActionMailer
  class Railtie < Rails::Railtie
    initializer 'http_action_mailer.add_delivery_method', before: 'action_mailer.set_configs' do
      ActiveSupport.on_load :action_mailer do
        ActionMailer::Base.add_delivery_method(:http, HttpActionMailer::DeliveryMethod)
      end
...

これらのInitializerの順番を制御するためにRubyのTSortというライブラリを使っています。TSortはトポロジカルソートができるライブラリで、各ノードの関係性を使ったソートができます。

TSortの簡単な利用例は公式のリファレンスに書いてあるとおり、こんな感じです↓

require 'tsort'

class Hash
  include TSort
  alias tsort_each_node each_key
  def tsort_each_child(node, &block)
    fetch(node).each(&block)
  end
end

{1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort
# [3, 2, 1, 4]

例だと

という条件によってソートをしています。(※トポロジカルソートというのは、出力辺より前にそのノードを並び替えることらしいが、RubyのTSortはその逆順になっているっぽい)

initializerでは以下のようなCollectionクラスを設定してinitializerメソッドで定義されたInitializerクラスを良い感じにソートしています。

class Collection < Array
  include TSort

  alias :tsort_each_node :each
  def tsort_each_child(initializer, &block)
    select { |i| i.before == initializer.name || i.name == initializer.after }.each(&block)
  end

  def +(other)
    Collection.new(to_a + other.to_a)
  end
end

自分のブロックの前に実行すべきノードをselectで集めることで、before/afterの実行順序制御を実現しています。

このエントリーをはてなブックマークに追加