Rails勉強会@東京 第2回 (2)

前半 に続いて、Rails勉強会@東京 第2回をレポートする。

後半セッション

4つのテーブルに分かれた。でも、なんか"rails under production"は人が少なくて消滅したらしい。

  • Task T: Testingを読む
  • rails under "production"
  • Restwiki の実装と Giza
  • Action Packを読む

私は「Action Pack」に出て、でもGizaも面白そうだからそっちが佳境に入ったあたりで移動しようと思っていた。でも、なんか、気がついたらずっと「Action Pack」のほうにいて、Giza聞き逃した。

「Action Pack」セッションでは、実質のところ Action Pack に限定せず、リクエストをRailsが処理するプロセスをDispatcherあたりから順に追っていった。みんなノートPCを出してそれぞれ読みながら、プロジェクタでは高橋会長の画面を写して、会長が解説してくれるところにみんなで適当に口を挟んで進めていく。

Dispatcher

WEBrick, mod_ruby, FastCGI, SCGIのいずれから呼ばれたにしても、コネクタ固有の処理が終わった後、リクエストはcgi.rbのCGIオブジェクトか、それと互換なインターフェースを持つオブジェクトに変換されて、Dispatcherに送られる。Dispatcherの

def dispatch(cgi = nil, session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)

がその窓口だ。railsパッケージのlib/dispatcher.rbに書いてある。 私はAjpRailsの開発にあたってこの周辺は結構読んでいるのでいくらかサジェストできる部分もあった。

前半のAjpRailsセッションでも軽く触れたのだけど、このDispatcherまわりの実装は結構疑問だ。そもそもDispatcherが受け付けるのはCGIオブジェクトだし、

request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi)

とか書いてあって(dispatcher.rb:36)、DispatcherがCGIの仕様に強く依存している。AjpRailsの開発では、AJPっていうCGIとは似ても似つかないプロトコルCGIに適合させるのはアホらしいし、どこかに抜けがある気がして、Dispatcher相当物を自前で実装している。実際、Dispatcherが例外を捕捉してユーザーへの通知を作成している最中に再び例外が発生したとき、最後の砦として書いてあるのが

output.write "Status: #{status}\r\n"

がなんだな(dispatcher.rb:94)。これのせいで、SCGIなんかは「最後の砦」が効かない。mod_rubyでここに落ちたそれはそれで悲惨そうだ。ActionController::AbstractRequest, ActionController::AbstractResponseなんてものがあって、Rails内部で使用するのはあくまでもこれらが提供する抽象インターフェースであるにも関わらず、窓口部分で具象クラスがハードコードされているのはどうかと思うよ、DHH。

ともかくそんな感じで問題はありつつもrequest, resposneのオブジェクトが生成されて、最後には次のように委譲する。

ActionController::Routing::Routes.recognize!(request).process(request, response).out(output)

これはわかりやすく書くとこういうことになる。

controller = ActionController::Routing::Routes.recognize!(request)
response = controller.process(request, response)
response.out(output)

さて、ではこれを順に見ていこう。

Routing

ActionController::Routing::Routes.recognize!(request)が行うのはrouting。これは、要するに各アプリケーションのconfig/routes.rbで

map.connect ':controller/service.wsdl', :action => 'wsdl'
...
map.connect ':controller/:action/:id'

とか書いてある設定に従ってREQUEST_URIを解釈し、controllerとaction, その他のパラメータを同定する。戻り値はControllerのインスタンスだ。戻ってきたときには、controllerインスタンスには、呼ぶべきaction名だとか、パラメータだとか設定されている。

難しいのはrecoginize!の中の方だ。actionpackパッケージのlib/action_controller/routing.rbを見る(active_controller/routing.rb:466)。

alias :recognize! :recognize

orz. aliasする意味は? 後方互換性か何かだろうか?

それにしても、routingを処理することのどこがdestructiveなん? という指摘があった。

短いから全部引いてしまうと、active_controller/routing.rb:452-465はこうなっている。

def recognize(request)
  string_path = request.path  
  string_path.chomp! if string_path[0] == ?/  
  path = string_path.split '/'  
  path.shift  

  hash = recognize_path(path)  
  return recognition_failed(request) unless hash && hash['controller']  

  controller = hash['controller']  
  hash['controller'] = controller.controller_path  
  request.path_parameters = hash  
  controller.new 
end

前の方は単にrequest.pathをスラッシュ区切りの列と解釈してsplitしてるだけやね。で、頭の部分はルートディレクトリのせいで空になってるだろうから、shiftして捨てる。

次が問題で、このpathの列をrecognize_path(path)に掛ける。じゃあrecognize_pathはどこさ、と見渡すと見付からない。実は、recognize_pathは動的に生成されているのだ。アプリケーションの設定から。

恐怖のcode generation

map.connect ':controller/:action/:id'

Railsデフォルトのrouting設定だ。railsコマンドが生成したconfig/routes.rbにはこう書いてある。これを、例えば/admin配下に置きたければ、

map.connect 'admin/:controller/:action/:id'

とやればAdminControllerとか誤解されることなく、adminは単なるprefixと理解されるわけだ。概念は簡単で、Apache側でrewriteしてやるよりも容易で、Railsの配備を柔軟にしている。でも、中はすごいことになっている。

Routing内部の用語では、上でいうadmin, :controller, :action, :idの各々のことをcomponentという(ActiveController::Routing::Component)。このコンポーネントがadmin - :controller - :action - :idと連なったのが1つのrouteだ(ActiveController::Routing::Route)。map.connectは複数回呼べるから、Railsアプリケーション内部には複数のrouteが存在する。routeを束ねたのがroute setだ(ActiveController::Routing::RouteSet)。

で、さっきのrecognize_pathを呼ぶとRailsは手持ちのroute setを見て、マッチするrouteがあればそれにしたがってpathを解釈して、対応する具象コントローラークラスやアクション名を取り出してくれるわけだ。

じゃあ、recognize_pathはいつ作成されるのか。config/routes.rbを評価したときなのだ。routes.rbは丸ごと

ActionController::Routing::Routes.draw do |map|

というブロックで囲まれている。RoutesっていうのはActiveController::Routing::RouteSetのインスタンスだ。そのdrawメソッドはselfを引数にgiven blockをyieldする。

ブロック内部でmap.connect、つまりActionController::Routing::Routes.connectするたびに、Routesの中に文字列を解析して作ったrouteが溜っていく。そして、yieldし終わると、drawは最後にwrite_generation, write_recognitionを呼ぶ。

まぁ、route setっていうのは静的構造から言えば構文木とかオートマトンとかそんな感じのものだ。でも、毎回構文木を辿ったりオートマトンを走らせたりするのは遅い。パスの認識はRailsの処理のほとんど全てが通る道であるからして、ここが遅いと困ったことになる。だから、 write_generation, write_recognitionでコード生成してしまう のだ。

この辺はactionpackのlib/action_controller/code_generation.rbに書いてある。でも、CodeGeneratorクラスにはdefとかifとかbeginとか信じられないようなメソッドがあるし。あれか! Object#classといっしょに導入された「紛らわしくなければ予約語をメソッド名にできる」というあれか! でも、普通はできてもやらないよなー。さすがはRails。組み込みクラスを大胆に拡張してしまうだけではなかった。なんだか深追いしてはいけない匂いがぷんぷんする。

でもね、コードを生成して、evalして、どうもこのあたりがセーフレベルを上げるとRailsが動作しない原因ではなかろうかと感じたりする。

というわけで、「ActionPack」セッションでは、「なんだかすごいことをして、RouteSet#connectに渡した文字列がコンパイルされてRubyのメソッドになる」という理解に留まったのであった。よく分からないすごいことをして、ActiveRecord::Routing::Routes.recognize_pathが定義される。

Routingの続き

さて、ActiveRecord::Routing::RouteSet#recognizeに戻ろう。異常系を省くとこうなる。

   hash = recognize_path(path)
   controller = hash['controller']
   hash['controller'] = controller.controller_path
   request.path_parameters = hash
   controller.new

こういうのを"hash"とネーミングしてしまうのは教育上よくないねっていう指摘あり。

hashを"controller"で引くと、pathのうち:controllerに相当する部分の文字列からactive_supportのマジックで取得された具象コントローラークラスが出てくる。なんかよくわからないけれど、これをcontroller_pathで置き換えている。

controller_pathはController::Baseのクラスメソッドで、クラス名から想定されるコントローラー定義ファイルへパスを復元するものらしい。ModuleA::ModuleB::FooBarControllerだったら、"module_a/module_b/foo_bar"が返る。

で、処理が終わったら、request.path_parametersにhashを代入してっと。あ、path_parameterはAjpRailsのデバッグで使ったから知ってる。QUERY_STRINGやReuqest Bodyと合わせて、後でrequest.paramsの値を作るときに使うのだ。

そして、最後は、おもむろに具象コントローラークラスをインスタンス化する。

controller.new 

process

かくして、コントローラーのインスタンスができたのであった。Dispatcherによれば次はこれに対してrequest, responseを引数にprocessを呼ぶわけね。ActiveController::Base#processの定義はこうなっている(active_controller/base.rb:361)。

def process(request, response, method = :perform_action, *arguments)

それで、本質的なところだけ抜き出すと、

def process(request, response, method = :perform_action, *arguments)
  send(method, *arguments)
  @response
end

ふーん。普通はperform_actionが呼ばれるわけだ。じゃあそっちを見よう。

def perform_action
  if self.class.action_methods.include?(action_name) ||
    self.class.action_methods.include?('method_missing')
    send(action_name

    render unless performed?
  elsif template_exists? && template_public?
    render
  else
    raise UnknownAction, "No action responded to #{action_name}", caller
  end
end

action_methodsはpublicなメソッドのうちActionController::Base.hide_methodで陽に隠蔽したメソッドをのぞく「アクションとして認識されるべきメソッド」を返してくれる。 もし、要求されたアクション名がアクションメソッドであるならば実際に実行する。終了後、ユーザーが明示的にrenderを呼んでいなければ、デフォルトのテンプレートを使用してrenderする。"if performed?"は内部の「レンダリング済みフラグ」をチェックして二重レンダリングを防止する。

それで、呼ばれたアクション名に対応するメソッドが定義されていなかったら、そのままデフォルトのテンプレートのrenderに移る。

このへんが、railsレンダリングのフレキシブルさのsourceであるらしい。

Rendering

ふんふん。じゃあ、renderだな。まぁ、ActionController::Base#renderの挙動はみなさん良く御存じだろう。中を見てみると引数に応じて地道にdispatchしている。render :xxx, ... を、後ろの方の引数を適当に処理しながらrender_xxxにdispatchする。この辺は普段触っているAPIだけ見ていても想像に難くないだろう。

render_xxxは基本的にテンプレートのほうの同名のメソッドに処理を委譲する。テンプレートは、というと、ActionView::Baseのサブクラスのインスタンスで、このへんがまた面白そうだったのだけれど、深く入っていく前に時間切れとなった。

他のセッション

  • production

    自然消滅

  • Testingを読む

    AWDwR chap.12を淡々と読んだらしい。章の構成がよくできてるな、ということでした。

  • Restwiki,Giza

    淡々と読んだらしい。GizaJavaScriptな部分と、サーバー側との住み分けがどうのとか聞いた。

「Testingを読む」のオーナーだったhs9587さんより、「セッションのオーナーになるのをそんなに難しく考えなくていい。自分がよくわからないところを『誰か教えて欲しい』といって開くのもアリだ。みんな、もっとオーナーになってほしい」とのアドバイス

懇親会( =宴会)

仕事のJavaが苦しい件とか、Rubyカンファレンスを数年のうちに日本で主催する計画だとか、色々喋った。途中でWeb+DB Pressの人がいらっしゃった。

ネットワーク構成の素晴らしい助言をいただいて、とてもためになり。それから、この間オブジェクト倶楽部のイベントで知り合った方とは大変楽しくお話しした。この方は中央線の事故で遅れて、どうしたかと思ってちょっと気を揉んでいたのだけれど、前半セッションの途中から出ていらして、たくさんお話できてよかった。

ライセンス

これだけRailsのコードから持ってきて改変したりすると、この記事がRailsの派生著作物になりそうなので、一応Rails著作権及びライセンスを書いておきます。

The copyright notice and the permission notice for rails package and actionpack package.
# Copyright (c) 2004 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

この記事にはRailsのライセンスは適用されません。そうですねー、この記事本体は「クリエイティブ・コモンズ第2.1版、日本法準拠版ライセンス 帰属 - 派生禁止」とします。