developmentを実現したいのでコードを読んでみる#4

だんだん、ソースコードを読むだけではしんどくなってきたので、ちょっとずるをします。

先日rails new --devの使い方を書きましたが、
http://d.hatena.ne.jp/akm/20120110#1326216516

railsの調査用のコードを入れまくって、ブログに書こうとおもって、forkしたので、それを参照するように--devオプションを使ってrailsアプリを作り、それをちょっと動かしてどこから呼び出されているのかを調べちゃおうという魂胆です。


ActionDispatch::Reloaderに対してはこんな感じの出力を追加しました。
https://github.com/akm/rails/commit/b0e5006cd2797c634cb596e7c3cddfa1fc3ffb44


これによって得られた情報からどんな風に使われるのかがある程度分かってくるのですが、まだ追いきれてないのでまた明日。

rails new --devの使い方

$  git clone https://github.com/rails/rails.git
$  cd rails/
$  git branch -r
$  git checkout 3-2-stable
$  cd ..
$  ruby rails/railties/bin/rails new rails3_20120110 --dev

これでcloneしたリポジトリの(3-2-stableブランチ)を参照するrailsアプリが生成できます。

http://edgeguides.rubyonrails.org/3_0_release_notes.html#living-on-the-edge

developmentを実現したいのでコードを読んでみる#3

昨日は、ActionDispatch::Reloader で使われている ActiveSupport::Callbacks のドキュメントを読みました。

今日はそれが ActionDispatch::Reloader でどう使われているのかを追いかけたいと思います。
https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/reloader.rb

まず定義されているコールバックは、 :prepare と :cleanup の2つです。
どちらも :scope => :name というオプションが指定されているので、コールバックで呼び出されるのがオブジェクトの場合、それぞれ#prepare と #cleanup メソッドが呼び出されます。

で、コールバックを登録するのは、 to_prepare と to_cleanup というクラスメソッドです。

なので、to_prepare, to_cleanup, prepare!, cleanup! を呼び出している箇所が分かればいいような予感がします。

to_prepare
-*- mode: grep; default-directory: "~/.rvm/gems/ruby-1.9.2-head@tengine_console/gems/" -*-
Grep started at Wed Jan 11 00:42:47

grep -nr to_prepare actionpack-3.1.3/lib railties-3.1.3/lib
actionpack-3.1.3/lib/action_dispatch/middleware/callbacks.rb:11:      delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
actionpack-3.1.3/lib/action_dispatch/middleware/reloader.rb:34:    def self.to_prepare(*args, &block)
railties-3.1.3/lib/rails/application/finisher.rb:22:      initializer :add_to_prepare_blocks do
railties-3.1.3/lib/rails/application/finisher.rb:23:        config.to_prepare_blocks.each do |block|
railties-3.1.3/lib/rails/application/finisher.rb:24:          ActionDispatch::Reloader.to_prepare(&block)
railties-3.1.3/lib/rails/application/finisher.rb:59:      # Force routes to be loaded just at the end and add it to to_prepare callbacks
railties-3.1.3/lib/rails/application/finisher.rb:65:        ActionDispatch::Reloader.to_prepare(&reloader)
railties-3.1.3/lib/rails/railtie/configuration.rb:55:      # Array of callbacks defined by #to_prepare.
railties-3.1.3/lib/rails/railtie/configuration.rb:56:      def to_prepare_blocks
railties-3.1.3/lib/rails/railtie/configuration.rb:57:        @@to_prepare_blocks ||= []
railties-3.1.3/lib/rails/railtie/configuration.rb:62:      def to_prepare(&blk)
railties-3.1.3/lib/rails/railtie/configuration.rb:63:        to_prepare_blocks << blk if blk
railties-3.1.3/lib/rails/railtie.rb:77:  #     # Add a to_prepare block which is executed once in production
railties-3.1.3/lib/rails/railtie.rb:79:  #     config.to_prepare do
to_cleanup
-*- mode: grep; default-directory: "~/.rvm/gems/ruby-1.9.2-head@tengine_console/gems/" -*-
Grep started at Wed Jan 11 00:44:47

grep -nr to_cleanup actionpack-3.1.3/lib railties-3.1.3/lib
actionpack-3.1.3/lib/action_dispatch/middleware/callbacks.rb:11:      delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
actionpack-3.1.3/lib/action_dispatch/middleware/reloader.rb:40:    def self.to_cleanup(*args, &block)
railties-3.1.3/lib/rails/application/bootstrap.rb:55:        ActionDispatch::Reloader.to_cleanup do
parepare!
-*- mode: grep; default-directory: "~/.rvm/gems/ruby-1.9.2-head@tengine_console/gems/" -*-
Grep started at Wed Jan 11 00:45:24

grep -nr "prepare!" actionpack-3.1.3/lib railties-3.1.3/lib
actionpack-3.1.3/lib/action_controller/metal/testing.rb:21:      @_response.prepare!
actionpack-3.1.3/lib/action_dispatch/http/response.rb:192:    alias prepare! to_a
actionpack-3.1.3/lib/action_dispatch/middleware/reloader.rb:23:  # middleware stack, but are executed only when +ActionDispatch::Reloader.prepare!+
actionpack-3.1.3/lib/action_dispatch/middleware/reloader.rb:45:    def self.prepare!
railties-3.1.3/lib/rails/application/finisher.rb:41:        ActionDispatch::Reloader.prepare!
railties-3.1.3/lib/rails/console/app.rb:30:  ActionDispatch::Reloader.prepare!
cleanup!
-*- mode: grep; default-directory: "~/.rvm/gems/ruby-1.9.2-head@tengine_console/gems/" -*-
Grep started at Wed Jan 11 00:45:58

grep -nr "cleanup!" actionpack-3.1.3/lib railties-3.1.3/lib
actionpack-3.1.3/lib/action_dispatch/middleware/reloader.rb:24:  # or +ActionDispatch::Reloader.cleanup!+ are called manually.
actionpack-3.1.3/lib/action_dispatch/middleware/reloader.rb:50:    def self.cleanup!
actionpack-3.1.3/lib/action_dispatch/middleware/reloader.rb:62:        ActionDispatch::Reloader.cleanup!
railties-3.1.3/lib/rails/console/app.rb:29:  ActionDispatch::Reloader.cleanup!

それぞれ調べないといけないんですが、

railties-3.1.3/lib/rails/console/app.rb:29:  ActionDispatch::Reloader.cleanup!
railties-3.1.3/lib/rails/console/app.rb:30:  ActionDispatch::Reloader.prepare!

で、rails consoleでの reload! の実装として使われているのが発見できました。
https://github.com/rails/rails/blob/3-1-stable/railties/lib/rails/console/app.rb

今日はハードな一日だったのでまた明日。

developmentを実現したいのでコードを読んでみる#2

昨日はRails::Applicationの継承関係をはっきりさせて、初期化のあたりをどうなっているのかRails Guideのドキュメントをみつけてわーいってところまで行きました。

Rails::Railtie <|---- Rails::Engine <|---- Rails::Application

http://guides.rubyonrails.org/initialization.html


で、本題は何だったのかっていうと、

./application.rb:168:        middleware.use ::ActionDispatch::Reloader unless config.cache_classes

でございます。

初期化周りは上のドキュメントとコードをざっくり読んで分かった気になったけど、実はたぶん分かっていないことが分かってはいるけど、あえて分かったように振る舞ってみることで先に進んじゃいます。

というわけで今日は本丸 ActionDispatch::Reloader のコードを読みます。

ActionDispatch::Reloader

https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/reloader.rb

分かった気になっているので、誤解を恐れず書いてみるとRack::Serverを継承したRails::Serverのインスタンスがリクエストを処理する際にappをcallするのですが、そのappにはmiddlewareがわらわらとくっついていて、callの呼び出しに対してフィルタをかける感じです。

そのmiddlewareの一つが読もうとしているActionDispatch::Reloaderです。なのでcallメソッドが理解できれば良いはずなんですが、

    def call(env)
      run_callbacks :prepare
      response = @app.call(env)
      response[2].extend(CleanupOnClose)
      response
    rescue Exception
      run_callbacks :cleanup
      raise
    end

最初のrun_callbacksでいきなり躓きましたwこれはきっと、includeされているActiveSupport::Callbacksで定義されているメソッドなんでしょう。

ActiveModel::Callbacksもこいつを使ってるのは知ってたのですが、ここでも出てきやがりました。ちょっとActiveSupport::Callbacksをやっつけましょう。

ActiveSupport::Callbacks

https://github.com/rails/rails/blob/3-1-stable/activesupport/lib/active_support/callbacks.rb

ソースコードを見ると結構難しいことをたくさんやっている感じなので、まずはドキュメントを見てみましょう。

http://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html のExampleを見てみると、define_callbacks でコールバックのkindだけ定義して、set_callbackでコールバックのkindに対して、いつ何をするのかを登録できる。で、実際に動くときにrun_callbacksをkindを指定して実行すると、set_callbackでkindに対して登録されたコールバックが呼び出される、って感じですね。

ポイントはrun_callbackがインスタンスメソッドなのに対して、
define_callbacks と set_callback はクラスメソッドってこと。なので、上のリンク先には後者は書いてなくて、 http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html
に説明が書いてあります。

ざーっと読んだ感じだと、 ActiveSupport::Callbacks.define_callbacks の :scope オプションが難しいっすね。
http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-define_callbacks

define_callbacks :save, :scope => [:kind, :name]

という風に定義されて、

set_callback :save, :before, Audit.new

という風にコールバック用のオブジェクトが指定されていたら、 Audit#before_saveメソッドが実行されると。

define_callbacks :save, :scope => [:kind]

ならAudit#beforeで

define_callbacks :save, :scope => [:name]

ならAudit#saveなんだそうですよ。

kindが before/after/aroundで、nameが指定されたdefine_callbackに指定されたcallbacksの要素ってことですね。

おっけー。じゃあ気を取り直して ActionDispatch::Reloader を読もう!と思ったけど、明日早いのでもう寝ます。また明日ー。

developmentを実現したいのでコードを読んでみる#1

Railsのdevelopmentモードのように特定のディレクトリ以下のソースコードを適切なタイミングで読み直す機能を作りたいのですが、実際Railsって何やっているのか分からんので調べます。

cache_classes

railsアプリの config/environments/development.rb には大抵

  config.cache_classes = false

と書いてあります。クラスのキャッシュを無効にするって意味っすね。

この設定がどこで使われているのか、railsの設定はrailtiesに書いてあるのでgrepしてみました。

-*- mode: grep; default-directory: "~/.rvm/gems/ruby-1.9.2-head@tengine_console/gems/railties-3.1.3/lib/rails/" -*-
Grep started at Sun Jan  8 20:35:42

grep -nri cache_classes .
./application/bootstrap.rb:64:        ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load
./application/configuration.rb:9:                    :cache_classes, :cache_store, :consider_all_requests_local,
./application/configuration.rb:95:        self.cache_classes = true
./application/finisher.rb:49:        if config.cache_classes && !$rails_rake_task
./application/finisher.rb:70:        if config.cache_classes && !config.dependency_loading
./application.rb:22:  # "allow_concurrency", "cache_classes", "consider_all_requests_local", "filter_parameters",
./application.rb:168:        middleware.use ::ActionDispatch::Reloader unless config.cache_classes
./generators/rails/app/templates/config/environments/development.rb.tt:7:  config.cache_classes = false
./generators/rails/app/templates/config/environments/production.rb.tt:5:  config.cache_classes = true
./generators/rails/app/templates/config/environments/test.rb.tt:8:  config.cache_classes = true
./railtie/configuration.rb:39:      # Third configurable block to run. Does not run if config.cache_classes
実はloadが使われる

developmentモードではActiveSupport::Dependenciesはrequireではなく、loadを使ってロードします。
ActiveSupport::Dependenciesが動く場合ってことは、const_missingあたりから命名規則にしたがってファイルをロードするあたりの話っすね、たぶん。

./application/bootstrap.rb:64:        ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load
middleware.use ::ActionDispatch::Reloader
./application.rb:168:        middleware.use ::ActionDispatch::Reloader unless config.cache_classes

一番大事そうなのはココ。developmentモードではミドルウェアがActionDispatch::Reloaderを使うそうです。
middlewareってRackとかの話だよね?これまでちゃんと調べたことなかったので、middlewareを調べましょう!

middleware

まず、ここに登場しているmiddlewareは何かと言えば、ここに書いてある。
https://github.com/rails/rails/blob/3-1-stable/railties/lib/rails/application.rb#L146

    def default_middleware_stack
      ActionDispatch::MiddlewareStack.new.tap do |middleware|
        if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache
          require "action_dispatch/http/rack_cache"
          middleware.use ::Rack::Cache, rack_cache
        end
        #...
      end
   end

ActionDispatch::MiddlewareStack.new.tapに渡されるブロックの引数でした。

ActionDispatch::MiddlewareStack

これは何ぞ?と検索してみる。ActionDispatchだからactionpack以下にあるはず・・・
https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb
ココですね。

ActionDispatch::MiddlewareStackクラスの定義の中に、Middlewareクラスの定義があって、ActionDispatch::MiddlewareStackクラスの具体的な記述は
https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb#L53
以降に書かれています。

include Enumerableとかしてるし、initializeメソッドで

@middlewares = []

とかやっているので、前述のMiddlewareクラスのオブジェクトを複数個持ってなんかする奴なんでしょうな。

具体的な使われ方として、

middleware.use ::ActionDispatch::Reloader

が意味するところをまずは知りたいんだけど、useメソッドの定義を読むと引数とブロックを、前述のMiddleware.newの引数に渡しちゃってmiddlewareを生成してそれをmiddlewaresに追加してるってことっすね。

    def use(*args, &block)
      middleware = self.class::Middleware.new(*args, &block)
      middlewares.push(middleware)
    end

https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb#L104

ActionDispatch::MiddlewareStack::Middleware

じゃあその前述のMiddlewareを知っておきたいところなんだけど、
https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb#L6

      def initialize(klass_or_name, *args, &block)
        @klass = nil

        if klass_or_name.respond_to?(:name)
          @klass = klass_or_name
          @name  = @klass.name
        else
          @name  = klass_or_name.to_s
        end

        @classcache = ActiveSupport::Dependencies::Reference
        @args, @block = args, block
      end

引数の最初にClassかクラスの名前を期待していて、Classが指定された場合は@klassに代入されるけど、そうじゃない場合は@klassはnilのまま。
それ以外はそのまま@argsに代入される。こいつはどこで使われるかって言うと、

      def build(app)
        klass.new(app, *args, &block)
      end

buildメソッドで指定されたklass.newの引数として、このメソッドの引数appとともに@argsの内容が渡されるんだけど、klassメソッドにはこう書いてある。

      def klass
        @klass || classcache[@name]
      end

名前を指定した場合には、classcacheから検索するようになっています。
classcacheはinitializeで指定されているActiveSupport::Dependencies::Referenceですね。

middleware.use ::ActionDispatch::Reloader

もう一度考えてみると、ActionDispatch::MiddlewareStackのインスタンスであるmiddlewareに::ActionDispatch::Reloaderをクラスを指定してuseさせているので、

ActionDispatch::MiddlewareStack::Middleware.new(::ActionDispatch::Reloader)

で生成されたものがmiddlewareには記憶されている。

あとは、どこかでこれのbuildメソッドが呼び出されるタイミングがあるはずなんだけど、
実は ActionDispatch::MiddlewareStack#build で ActionDispatch::MiddlewareStack::Middleware#build が呼び出されます。
https://github.com/rails/rails/blob/3-1-stable/actionpack/lib/action_dispatch/middleware/stack.rb#L109

    def build(app = nil, &block)
      app ||= block
      raise "MiddlewareStack#build requires an app" unless app
      middlewares.reverse.inject(app) { |a, e| e.build(a) }
    end

こいつはどこから呼び出されるのか?これはRails::Engine#appから呼び出されます。

Rails::Engine

railtiesをActionDispatch::MiddlewareStackでgrepしてみると、以下の2つが見つかります。

-*- mode: grep; default-directory: "~/.rvm/gems/ruby-1.9.2-head@tengine_console/gems/railties-3.1.3/lib/" -*-
Grep started at Sun Jan  8 22:52:59

grep -nri ActionDispatch::MiddlewareStack .
./rails/application.rb:146:      ActionDispatch::MiddlewareStack.new.tap do |middleware|
./rails/engine.rb:606:      ActionDispatch::MiddlewareStack.new

それぞれ Rails::Appliation#default_middleware_stack と Rails::Engine#default_middleware_stack から呼び出されています。

Rails::Appliation? そうです。railsアプリのconfig/application.erbに記述されるアレです。

module Blog
  class Application < Rails::Application

https://github.com/rails/rails/blob/master/railties/guides/code/getting_started/config/application.rb#L12


で、この Rails::Application は Rails::Engine を継承しているわけですね。
https://github.com/rails/rails/blob/3-1-stable/railties/lib/rails/application.rb#L36

じゃあこの Rails::Engine はというと、 Rails::Railtie を継承しています。
https://github.com/rails/rails/blob/3-1-stable/railties/lib/rails/engine.rb#L333

Rails::Railtie は何も継承していません。
https://github.com/rails/rails/blob/3-1-stable/railties/lib/rails/railtie.rb#L113

まとめるとこういう継承をしているわけですね。

Rails::Railtie <|---- Rails::Engine <|---- Rails::Application

Rails::Applicationのインスタンスがいつ生成されるのかが知りたくなるわけですが、これを追っかけるのは大変!と思っていたら強い味方発見。
http://guides.rubyonrails.org/initialization.html
すばらしい!


続きはまた明日。

イケテルOSSソースコードリーディング勉強会

明日はグローバルブレインズさんが公開したプロダクトWaRKSのソースコードリーディングですよ。
http://atnd.org/events/22038

githubで公開されているので、 僕もTravis CIの勉強がてらpull requestを送ってみました。
https://github.com/akm/WaRKS

明日の13:00から博多駅近くのRubyコンテンツセンターです。

Rails3.1アプリの本番サーバにJSのエンジンを入れない方法

rails-3.1 いえーい!っていうわけでついついcoffee scriptを使ったんだけど、ステージング環境のRedhatにデプロイしてみたら、画面上で動くはずのJSが動かない。
見てみるとapplication.jsの中身が

throw Error("ExecJS::RuntimeUnavailable: Could not find a JavaScript runtime. See https://github.com/sstephenson/execjs for a list of available runtimes.\n  (in /home/tengine/sources/tengine_console/app/assets/javascripts/application.js)")

という一行だけ。

https://github.com/sstephenson/execjs を読んでみると、JSのエンジンをインストールしとろか言ってる。
ちょw、RailsサーバにJSのエンジンなんてインストールしたくないんですけど?

ググってみるとそんな情報だらけ。うーん、本番でそんなことしたら怒られる、っていうか僕が怒る。

しょうがないので自力での解決を目指す。

まずは

rake -Tでrake assets:precompile

を見つけたのでやってみた。public/assets以下にちゃんと uglifierによってぐちゃぐちゃに短くなったapplication.jsが作られた。

開発環境でproductionで画面を動かしてみる。ちゃんと動く。でもapplication.jsはとってもキレイ。再生成されてやがる。そんなのしたくないんだってば。public/assets/application.js を返してよ。

ソースコードgrepしてみると、execjs-1.2.9/lib/execjs/runtime.rb の ExecJS.autodetect で例のメッセージを出してやがっている。そいつが呼ばれるのは、なんとexecjs.rbがロードされたときだ。

じゃあロードされないようにすればいい。リリースするパッケージに execjsやcoffee-scriptなどが含まれないように、

bundle install --path vendor/bundle --without development test assets

としてGemfile.lockから再生成させる

vendor/bundleには確かに不要なgemファイルは入らないが、Gemfile.lock には思いっきり書かれている。大丈夫なのか?使われちゃうんじゃないの?
調べてみると、本家にちゃんと書いてあった http://gembundler.com/rationale.html
FAQ: Why Is Bundler Downloading Gems From --without Groups?

デプロイ時に依存するものが変わっちゃって、bundlerの利点がないからだって。
まあ、そういうもんなんだろうね。了解。

じゃあ本番に持っていく必要なgemを全てそろえたパッケージを

bundle package

で作成する。vendor/cacheにdevelop, test, assetsのgemがコピーされる。
どうせ使われないのは分かっているけど、tarで固めてコピーする

で、本番環境で、

bundle install --gemfile ./Gemfile --path ./vendor/bundle --deployment --without development test assets

とかやってセットアップすれば、vendor/bundleにちゃんと使用するものだけが展開される。




前後しちゃったけど、config/environments/production.rbの以下の場所を変更する必要もある。

  # Disable Rails's static asset server (Apache or nginx will already do this)
  config.serve_static_assets = true

  # Compress JavaScripts and CSS
  config.assets.compress = false