- Rack を使って Web サーバで統一されたインターフェイスの利用する
- http://d.hatena.ne.jp/secondlife/20070307/1173253661
- Rack:a Ruby Webserver Interface
- http://rack.rubyforge.org/
Rack を使えば生 Ruby で WEBrick や Mongrel や FastCGI やいろいろあっても統一したインターフェースで書けるよ!
デバッグの時は WEBrick の方が挙動が落ち着いてるしスタックトレースも見やすいし。
でも本番はやっぱりせめて Mongrel か、ちょっと面倒だけど FastCGI がええな〜。
と啓蒙されたので最近 Rack を使い始めたんだが、ドキュメントがまるっきりない……
とりあえずセッションを使いたくて、Rack::Session::Cookie というのがそれっぽいのだが、ドキュメントがかけらもない。
しかたなくソースを読んで、しばし瞑想。うーん、こうかな?
require 'rubygems' require 'rack' test_app = Proc.new do |env| session_data = env["rack.session"] session_data["counter"] ||= 0 session_data["counter"] += 1 Rack::Response.new("counter : #{session_data["counter"]}").finish end app = Rack::URLMap.new([['/test', Rack::Session::Cookie.new(test_app)]]) Rack::Handler::Mongrel.run app, :Port => 11080
お、いけたいけた。リロードするたびにカウントアップ。
でも、Rack::Session::Cookie はセッションの内容を Marshal して cookie に積んでるだけなので、ちょっと苦しい。
やっぱりちゃんとセッションIDを発行してもきょもきょやりたいところ。
というわけで、Rack::Session::Cookie と CGI::Session を参考に、セッションIDに対応した Proc を作ってくれるクラスを書いてみた。
まだプチ試作なので、最適化が足りなかったり、セッションデータの expire が考慮されていなかったりするあたりは勘弁。あーあとちゃんとスレッドセーフかどうかも検証しきれてないかも( Hash#[] がスレッドセーフならおおむね大丈夫だと思うんだが)。
[3/18] Proc を継承する必要ないんじゃない、とのご指摘をいただいたので修正。
require 'rubygems' require 'rack' module Rack class SessionProc @@sessiondata = {} def initialize(opt={}, &proc) @proc = proc @key = opt[:key] || "rack.session.id" @default_options = {:domain => nil, :path => "/", :expire_after => nil}.merge(opt) end def call(env) load_session(env) status, headers, body = @proc.call(env) commit_session(env, status, headers, body) end private def load_session(env) request = Rack::Request.new(env) session_key = request.cookies[@key] env["rack.session.options"] = @default_options.dup session_data = env["rack.session"] = (@@sessiondata[session_key] || {}).dup def session_data.create_new_id(renew=false) return @old_session_id if !renew && @old_session_id require 'digest/md5' md5 = Digest::MD5::new now = Time::now md5.update(now.to_s) md5.update(String(now.usec)) md5.update('foobar') md5.update($$.to_s) md5.update(String(rand(0))) @new_session_id = md5.hexdigest end def session_data.new_session_id @new_session_id end def session_data._init(id) @new_session_id = nil @old_session_id = id puts "@old_session_id: #{@old_session_id}" self end session_data._init session_key end def commit_session(env, status, headers, body) session_data = env["rack.session"] request = Rack::Request.new(env) session_key = request.cookies[@key] if session_data.new_session_id @@sessiondata.delete(session_key) if session_key session_key = session_data.new_session_id puts "session_key: #{session_key}" @@sessiondata[session_key] = session_data options = env["rack.session.options"] cookie = {:value => session_key} cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil? response = Rack::Response.new(body, status, headers) response.set_cookie(@key, cookie.merge(options)) return response.to_a elsif session_key && @@sessiondata[session_key] != session_data @@sessiondata[session_key] = session_data end [status, headers, body] end end end
こんな風に使う。
普通に Proc.new するところを Rack::SessionProc.new して、session_data に対して create_new_id してあげるだけ。
require 'rubygems' require 'rack' require "sessionproc.rb" # 追加 test_app = Rack::SessionProc.new do |env| session_data = env["rack.session"] session_data.create_new_id # セッションID生成。すでに生成されている場合は何もしない(引数に true を指定で強制生成) session_data["counter"] ||= 0 session_data["counter"] += 1 Rack::Response.new("counter : #{session_data["counter"]}").finish end app = Rack::URLMap.new([['/test', test_app]]) Rack::Handler::Mongrel.run app, :Port => 10080
WEBrick と Mongrel で動作確認。FastCGI はいけるんじゃないかと思うんだけど、未確認。
セッションデータはオンメモリで保持しているので、CGI はアウトな気がする。