禾川資訊 Grass Brook

Text

研究 Rails 3 的 boot process 和 initialization process

徹底了解 Rails 3 的 boot/initialization 流程對於控制 Rails 應用程式是很重要的一個課題,當我們下了 rails server 命令,究竟發生了什麼事,Bundler 和 Rack 又扮演了什麼角色,讓我們來瞧個仔細。

說明:環境變數 GEM_PATH 是 rubygems 的安裝路徑,ROOT_PATH 是應用程式根目錄,而本篇文章使用 RAILTIES_PATH 代表 railties 這個 gem 的根目錄,依此類推。引述段落則是依序指出重點檔案或指令。

在 Rails 3 應用程式根目錄下,執行 rails server 指令,第一步便是透過 boot.rb 誘發 bundler 將所有相關 rubygems 的 lib 都加到 $LOAD_PATH:

GEM_PATH/bin/rails server
RAILTIES_PATH/bin/rails
ROOT_PATH/script/rails
#> also define ENV_PATH, BOOT_PATH, APP_PATH
ROOT_PATH/config/boot.rb
#> add required gems’s lib into $LOAD_PATH

bundler 預設用 Definition.from_gemfile 解讀 ROOT_PATH/Gemfile,再以 Bundler.setup 方法呼叫 Bundle::Runtime 設定 rubygems;或是先執行 bundle lock 產生 ROOT_PATH/.bundle/environment 檔案(由 Definition.from_lock 解讀 ROOT_PATH/Gemfile.lock,再由 Bundle::Runtime 生成),當被 require 時會執行新的 Bundler.setup 方法設定 rubygems。

而 ROOT_PATH/script/rails 最後所 require 的 commands.rb 便會依序載入 rails.rb,設定基礎 Rails 環境,加載 rails/application.rb 及各式 Railtie,掛載透過 bundler 設定的 rubygems,啟動 Rails::Server 通知 Rack::Server 跑 Rails::Application:

RAILTIES_PATH/lib/rails/commands.rb
RAILTIES_PATH/lib/rails/commands/server.rb
rails_server = Rails::Server.new #=> also set ENV[“RAILS_ENV”]
ROOT_PATH/config/application.rb
RAILTIES_PATH/lib/rails/all.rb
RAILTIES_PATH/lib/rails.rb
ACTIVESUPPORT_PATH/lib/active_support.rb
……
RAILTIES_PATH/lib/rails/application.rb
……
Bundler.require :default, Rails.env
……
rails_server.start

這時 Rack::Server.start 會呼叫 .app 實體方法,先讓 Rack::Builder 以 #parse_file 類別方法解讀 config.ru,產生 Rakc::Builder 實體物件。此 Rack::Builder 物件會載入 environment.rb 來初始化 YourApp::Application,用 .run 實體方法把 YourApp::Application 這個龐大的 Rack application(可回應 call 方法傳入 env 參數並回傳 [status, header, body] 的 Ruby 實體物件/Proc 物件)也加入 Rack::Builder@ins 陣列,並回傳成 Rack::Server@app 陣列。最後 Rack::Server 透過 .wrapped_app 實體方法把 Rails::Server 的 middlewares 包裹成 @app 陣列,並實體化為 rack application,一起丟給 Rack::Handler 處理:

rails_server.start
……
wrapped_app = rails_server.build_app(@app)
#> @app = Rack::Builder.parse_file(‘config.ru’) = YourApp::Application
##> ROOT_PATH/config.ru
##> ROOT_PATH/config/environment.rb
##> YourApp::Application.initialize!
##> run YourApp::Application
#> rails_server.middleware[‘development’]
rack_app = wrapped_app = #<Rails::Rack::LogTailer:0x2f92228 @app=YourApp::Application, @cursor=612423, @file=#<File:log/development.log>, @last_checked=1270047942.86726>
……
rails_server.server.run(wrapped_app)
#> Rack::Server.new.server.run(wrapped_app)
#> Rack::Handler.default(options).run(wrapped_app)
rails_server = #<Rails::Server:0x2dafa14 @app=YourApp::Application, @_server=Rack::Handler::Mongrel, @options={:server=>nil, :AccessLog=>[], :pid=>”tmp/pids/server.pid”, :Host=>”0.0.0.0”, :debugger=>false, :daemonize=>false, :environment=>”development”, :config=>”config.ru”, :Port=>3000}>
……
Rack::Handler::Mongrel.run(rack_app)
#> http_server = Mongrel::HttpServer.new
#> handler = Rack::Handler::Mongrel.new(rack_app)
#> handler = Rack::Chunked.new(Rack::ContentLength.new(rack_app))
#> http_server.register(‘/’, handler)
……
Rack::Handler::Mongrel.process

如果跑 mongrel server 則 Rack::Handler.default 會以 Rack::Handler::Mongrel.run 方法跑一個 Mongrel::HttpServer 實體物件,並把每個 rack application(獨立的 Rack::Handler::Mongrel 實體物件)都註冊起來。

當 mongrel server 收到 http request 後,會送給 Rack::Handler::Mongrel 的 .process 實體方法,呼叫 rack application 的 .call 實體方法送出 http response。因為 Rails 3 每個 controller action、metal 都被設計成 rack application,所以創造出無限可能。

另外要提的是 Phusion Passenger 2.2.9 之後也採用類似的方法跑 Rails 3 application(主要是 require 的順序不一樣):

PhusionPassenger::Railz::ApplicationSpawner#preload_application
ROOT_PATH/config/environment.rb
ROOT_PATH/config/application.rb
ROOT_PATH/config/boot.rb
….
YourApp::Application.initialize!
run YourApp::Application

以上是 rails boot process 的概要,接著來看 rails initialization process 的部份。當 Rack Server 在解讀 config.ru 時,會產生一個這樣的 rack application 物件:

rack_app = Rack::Builder.new {
  require ::File.expand_path('../config/environment',  __FILE__)
  run YourApp::Application
}.to_app

而 Rails::Application 初始化等於是展開 config.ru 這個 block 內容,最終會產生一個描述完整的 Rack::Builder 實體物件:

rack_app = Rack::Builder.new {
  use ActionDispatch::Static
  use Rack::Lock
  use Rack::Runtime
  use Rails::Rack::Logger
  use ActionDispatch::ShowExceptions
  use ActionDispatch::RemoteIp
  use Rack::Sendfile
  use ActionDispatch::Callbacks
  use ActionDispatch::Cookies
  use ActionDispatch::Session::CookieStore
  use ActionDispatch::Flash
  use ActionDispatch::ParamsParser
  use Rack::MethodOverride
  use ActionDispatch::Head
  use ActiveRecord::ConnectionAdapters::ConnectionManagement
  use ActiveRecord::QueryCache
  run YourApp::Application.routes
}.to_app

一切都從類別宣告開始。從 ROOT_PATH/config/environment.rb 把 ROOT_PATH/config/application.rb 給 require 後的宣告開始:

YourApp::Application < Rails::Application < Rails::Engine < Rails::Railtie

這 YourApp::Application 的繼承宣告主要做了幾件事,定義 called_from 變數,引入 Rails::Application::Configurable 模組,定義 #config 類別方法用來建立 Rails::Application::Configuration 實體物件,排 subclasses 順序,把自己定義成 Singleton,最後把自己實體化:

## snippet of Rails::Application
def inherited(base)
  raise "You cannot have more than one Rails::Application" if Rails.application
  super
  Rails.application = base.instance
end

## snippet of Rails::Engine
def inherited(base)
  unless base.abstract_railtie?
    base.called_from = begin
      # Remove the line number from backtraces making sure we don't leave anything behind
      call_stack = caller.map { |p| p.split(':')[0..-2].join(':') }
      File.dirname(call_stack.detect { |p| p !~ %r[railties[\w\-\.]*/lib/rails|rack[\w\-\.]*/lib/rack] })
    end
  end
  super
end

## snippet of Rails::Railtie
def inherited(base)
  unless base.abstract_railtie?
    base.send(:include, self::Configurable)
    subclasses << base
  end
end

每種 Railtie(Application, Engine, Plugin 都是一種 Railtie 型態)的 #config 類別方法都是在管理 Rails::Railtie::Configuration(子)物件。如呼叫 Rails::Application.config 時,會以 called_from 路徑中第一個有 config.ru 的路徑做為 root 參數建立 Rails::Application::Configuration 實體物件。

## snippet of Rails::Application::Configurable
def config
  @config ||= Application::Configuration.new(self.class.find_root_with_flag("config.ru", Dir.pwd))
end

## snippet of Rails::Application::Configuration
def initialize(*)
  super
  @allow_concurrency   = false
  @filter_parameters   = []
  @dependency_loading  = true
  @serve_static_assets = true
  @time_zone           = "UTC"
  @consider_all_requests_local = true
  @session_store = :cookie_store
  @session_options = {}
end

## snippet of Rails::Engine::Configuration
def initialize(root=nil)
  super
  @root = root
end

## snippet of Rails::Railtie::Configuration
def initialize
  @@options ||= {}
end

基本上各 Railtie 利用 #initializer 類別方法定義了許多初始化時要執行的 initializer block,當 YourApp::Application.initialize! 執行時,會透過 #method_missing 類別方法得到 YourApp::Application 實體物件,透過 railties.all 實體方法取得所有 Railtie 實體物件,接著取得各 initializers 實體物件,再依序執行這些 initializer block,完成整個應用程式初始化動作:

## snippet of Rails::Application
def self.method_missing(*args, &block)
  instance.send(*args, &block)
end

def initialize!
  run_initializers(self)
  self
end

def initializers
  initializers = Bootstrap.initializers_for(self)
  railties.all { |r| initializers += r.initializers }
  initializers += super
  initializers += Finisher.initializers_for(self)
  initializers
end

## Rails::Initializable
def run_initializers(*args)
  return if instance_variable_defined?(:@ran)
  initializers.each do |initializer|
    initializer.run(*args)
  end
  @ran = true
end

def self.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

而 initializers 實體物件群組執行順序是 Rails::Application::Bootstrap -> Rails::Railtie.subclasses -> Rails::Engine.subclasses -> Rails::Plugin.all -> Rails::Engine -> Rails::Application::Finisher。雖然 initializer 執行順序預設是依照宣告順序,也可用 :before, :after 參數排序,但 Railtie、Engine、Plugin 各群組內依名稱字母排序的特性,實際撰寫 Engine 或 Plugin 時還是要多留心一下:

Rails::Application::Bootstrap
ActiveSupport::Railtie
I18n::Railtie
ActionDispatch::Railtie
ActionView::Railtie
ActionController::Railtie
ActiveRecord::Railtie
ActionMailer::Railtie
ActiveResource::Railtie
Rails::TestUnitRailtie
AllKindsOfEnginesOrderedByAlphabet (Rails::Engine)
AllKindsOfPluginsOrderedByAlphabet (Rails::Plugin)
YourApp::Application (Rails::Engine)
Rails::Application::Finisher

其中 Rails::Engine 有個名為 :add_routing_paths 的 initializer 會處理各 engine 的 routes.rb 載入;另 Rails::Application::Finisher 中有個名為 :build_middleware_stack 的 initializer 會呼叫 Rails::Application 的 .app 實體方法將所有 default_middleware、Railtie 的 middlewares 或自定的 middlewares 都丟到一個 ActionDispatch::MiddlewareStack 陣列中,並建立這些 middlewares 實體物件,這都是值得留意的基本 initializer。當所有 initializers 都執行完畢,一個 Rails 3 的 rack application(@app)便建立完成,達到初始化的最終目的:

## Rails::Application
def app
  @app ||= middleware.build(routes)
end

def call(env)
  env["action_dispatch.parameter_filter"] = config.filter_parameters
  app.call(env)
end

接著就是搬椅子等待 Rack server 呼叫 Rails::Application 的 .call 實體方法囉。

更新:內文已更新為 rails-3.0.0.beta2 的版本為主。

Posted on Saturday, March 20 2010. Tagged with: bundlerrackrails 3rails boot processrails initialization process
禾川資訊 Grass Brook We are a studio focused on Ruby, Rails and Agile Development.
Ask me anything Submit
Previous Next