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
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
で、この 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
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