As Sloth As Possible

可能な限りナマケモノでありたい

タグ:Ruby

随分長いことブログ放置してしまったのだけどネタ見付けたので久々の記事。

UnicornはPassengerより遅かった?

なんかTwitterで「アクセス少ないときはPassengerよりUnicornのが速いのに、アクセス多くなってきたらその逆になった」って話をみかけたので、それ単にUnicornのworkerが足りないんじゃないの、と返したのだけど、どういうことかという話を少しまとめる。

まず、Unicornのworkerは1プロセスにつき1度に1リクエストしか処理しない。だから例えば、凄い大雑把な計算だけど、平均50msくらいでレスポンスを返すアプリだとすれば、1workerは20req/secくらいは返せるかなと見積もって、ピーク時に100req/secくらいアクセスがありそうだったらworkerを5個くらい立てとくかな、足んなかったらもうちょっとかな、みたいに考える。実際どんくらいのアクセスなのかは聞いてないので知らないけど、Nginxが10workerいるのにUnicornが2workerだって言ってたから、普通逆じゃない、とつっこんだ。Nginxは逆に1workerで複数リクエスト捌ける(数千くらい同時にアクセスされても耐えられるらしい)のでCPUのコア数と同じかちょっと多いくらいでよくて、Unicornのworkerの方をガンガン立ち上げるべき。

Passengerもそれ自体は同じなんだけど、Passengerの場合負荷上がってきたら空気読んで設定した上限までプロセス数を増やしてくれるので、「アクセス増えても強い」のではなくて「アクセス増えたらいつの間にか仲間呼んで倍の人数で戦ってた」とかそういうことじゃないかと思う。上記逆転が起こるのは「Unicornのworkerが少ない」「リクエスト数が増えても1個1個の処理時間は大して変わらない」「メモリやCPUには余裕がある」って条件のときだと思うので、多分Unicornのworker数を適切な数に調整すれば解決する。

workerの数増やしたらメモリ食うんじゃ、って言う話なら、Unicornはworkerプロセスを立ち上げる前にアプリをpreloadする仕組みがある。アプリ初期化時点で必要なライブラリを全部読み込んであれば、workerプロセスの起動は単なるforkなので、CopyOnWriteで親プロセスとメモリを共有して効率的に使ってくれそうな気配はある。それでも食うっちゃ食うけども、worker一杯立ち上げたらメモリ足んなくなるならPassengerでも同じ話。平常時には節約してくれるけども、裏を返せば急激なアクセス増大に際しては慌ててプロセスを沢山作るコストで負荷上がることもあるし、単体で比較するとUnicornのがまぁ普通に強い。大量アクセスがある程度定期的に来るの分かってるならUnicornで、滅多にないならお手軽なPassengerで、みたいな使い分け。

Rainbows!って何

ここまでの話はあくまで、1個1個のリクエストは大して重くないけど一度に沢山のリクエストが来ちゃったら遅くなった、みたいな場合。例えばさっきと違って、平均で見れば100msくらいなんだけど、20回に1回くらいの割合で1500msくらいかかるリクエストがくることがある、とする。それがたまたま5人同時にリクエストしてしまったら、5workerがそれぞれ1.5秒ずつ拘束されてその間一切レスポンスを返せなくなる。内部ではファイルサーバにデカいファイル書いてるとか、DBに重いクエリ投げてるとか、API叩いて外部からデータ取って来てるとかで、実際にはアプリ自体は待ってるだけでCPUもメモリもスッカスカ、みたいな状態であっても。それ以外のリクエストは30msくらいで即答できるのだとしても。

Rainbows!の場合はUnicornをベースにしているけれども、EventMachineとかRevacatorみたいなイベント駆動ライブラリを使って1workerで並列にリクエストを処理できるようにしている。さっきみたいに長時間待ちになるリクエストが来てても、待ってる間に他の数十ms程度で返せるリクエストを受け取って処理して返せる。1プロセス1リクエストの枷が外れるので素のUnicornよりはworker数減らせてコンパクトになるかもしれない。そしてイベントループの中でアプリが動くのでAsyncSinatraみたいなフレームワークを使ってアプリ内で非同期処理を書ける。なので重いリクエストがある場合はこっちを使うといい、って話になる。

なんだ良いことづくめじゃん、Unicornいらない子じゃん、ってなるかと思えばそうでもない。Unicornがそもそもpreforkで1プロセス1リクエストでみたいな、ひどく割り切った、見ようによっては割と古典的な仕組みになってるのは、先行していたイベント駆動型のThinみたいなアプリケーションサーバに対しての「シンプルな方が作るのも管理も楽だし、なんだかんだでパフォーマンスも出るじゃん」みたいなアンチテーゼ的なところだったりするので、わざわざ複雑さを戻してるRainbows!はそういうメリットとのトレードオフになる。使いこなすと素敵な感じにはなるけど、worker増やせば解決する程度の話なら素のUnicornでも大して困らない。

追記

「Unicornシグナル送ればプロセス増やしたり減らしたりできるから、netstatとか見て詰まってそうだったら自動で増えるようにしてるー。滅多に増えないけど」っていう情報を貰った。TTINでプロセス増やす、TTOUでプロセス減らすとかできるので、自動で増減して欲しければなんか適当に監視スクリプト書いて適宜シグナル送るようにしとけばいいかも。まぁ手で送ってもいいし。

こないだ、と言っても2週間くらい前の話なんだけど、社内でZeroMQMongrel2の勉強会をやった。Mongrelと言えば、俺がRails(たしか当時1.2くらいだったと思う)で仕事してた頃にアプリケーションサーバとして使ってたけど、最近だとThinとかPassengerとかUnicornとかの人気に押されてついぞ聞かなくなったアレだよなぁ、なんでPerlの会社の勉強会でMongrelなんだろう、と思ってたんだけど、Mongrel2はもはやRailsのアプリケーションサーバじゃなくて、通信にZeroMQなるものを使った汎用的なWebサーバになってたらしい。

大分野心的なプロジェクトではあるものの、今はZeroMQもMongrel2も「とりあえず出た」って感じらしく、今年一杯くらいは地雷原を突き進む気がある人だけ触るといいんじゃないかなという話だった。プロダクションで使うようなレベルになるにはもうしばらくかかりそうだけど、今なら各言語の実装も追い付いてないようなので、遊んでおくなら今のうち。いずれMongrel2が大流行したときに「faultierさん、是非本を書いてください!」ってお願いされることを夢見て色々ごにょごにょしてみたよ。

準備

何はともあれZeroMQとMongrel2をインストールする。とは言っても、Getting Startedの通りにインストールするだけ。例に書いてある奴は若干バージョンが古いので、それぞれの最新版を取ってきた方がいいと思う。ちなみにZeroMQはhomebrewにもFormulaがあった。pyzmqは無かったので自分で作るなどした。

今回はRubyで試すので、RubyのZeroMQバインディングも入れておく。これは普通にgem install zmqで入るはず。Rackのハンドラの例の方はffi-rzmqを使ってるんだけど、なんか手元の環境で上手くインストールできなかったのでそっちは試してない。まぁ今回やることにはどっちがどうとかあんまり関係ないのでzmqの方でいいか。

そこまでできたら今度はMongrel2の設定を用意する。なんでCで書かれてるはずのMongrel2がやたらとPythonのライブラリ入れまくるんだろうと思ったんだけど、Mongrel2の操作にはm2shというPythonで書かれたスクリプトを使うかららしい。設定の仕方が面白くて、まずはPythonで書かれた設定ファイルを用意して、それをm2shでsqliteのファイルに書き出し、それを使ってMongrelが起動する、というようになってる。だから多分m2sh相当のものをPerlなりRubyなりで用意してしまえば、別にPythonは必要ないはず。まぁ、面倒なので大人しくm2shを使う。設定ファイルはこんな感じにした。

# m2test.py
from mongrel2.config import *

main = Server(
    uuid ="2f62bd5-9e59-49cd-993c-3b6013c28f05",
    chroot="./",
    access_log="/logs/access.log",
    error_log="/logs/error.log",
    pid_file="/run/mongrel2.pid",
    default_host="localhost",
    name="main",
    port=6767,
    hosts=[
        Host(
            name="localhost",
            routes={
                r'/m2test': Handler(
                    send_spec="tcp://127.0.0.1:9997",
                    send_ident="70D107AB-19F5-44AE-A2D0-2326A167D8D7",
                    recv_spec="tcp://127.0.0.1:9996",
                    recv_ident=""
                )
            }
        )
    ]
)
settings = {"zeromq.threads": 1}
commit([main], settings=settings)

Mongrel2のexamplesに入ってたのを参考にした。なんとなくわかると思うけど、hostsの中のHandlerってやつが今から作るハンドラと通信する為の設定になる。これが出来たら、

$ mkdir run log tmp
$ m2sh init -db m2test.db
$ m2sh load -db m2test.db -config m2test.py
$ m2sh start -db m2test.db -host localhost

とかすると、Mongrel2が立ち上がる。http://localhost:6767/m2testでアクセスすると裏のハンドラに処理が渡るはずだけど、まだ作ってないのでこの時点ではレスポンスが返ってこず、延々待たされる。

プロトコルを調べる

Mongrel2は内部にアプリケーションサーバを持つわけではなく、基本的にやることはZeroMQを使った通信をするだけ。上に書いた設定だと、ローカルの9997ポートと9996ポートにZeroMQのソケットが用意されて、ハンドラは9997ポートからリクエストを受けとり、処理したら9996ポートにレスポンスを送ってやるようにする。ちなみにこのソケットは別にUnixソケットのことではなく、ファイルを経由したりプロセス内通信したりネットワーク越しに通信したり色々できるらしい。詳しくはZeroMQを調べてみるといいと思う。

リクエストのメッセージは、「センダのID コネクションのID パス ヘッダの長さ:ヘッダ,ボディの長さ:ボディ」の形式で飛んでくる。実際にはこんな感じ。

70D107AB-19F5-44AE-A2D0-2326A167D8D7 2 /m2test 542:{"PATH":"/m2test","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/m2test","PATTERN":"/m2test","Accept":"application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5","Accept-Charset":"Shift_JIS,utf-8;q=0.7,*;q=0.3","Accept-Encoding":"gzip,deflate,sdch","Accept-Language":"ja,en-US;q=0.8,en;q=0.6","Cache-Control":"max-age=0","Connection":"keep-alive","Host":"localhost:6767","User-Agent":"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_4; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.62 Safari/534.3"},0:,

んで、レスポンスは「センダのID コネクションIDの長さ:コネクションのID HTTP/1.1 ステータスコード ステータスメッセージ ヘッダ ボディ」という形式で返す。例えばokと返すだけのレスポンスならこんな感じ。

70D107AB-19F5-44AE-A2D0-2326A167D8D7 1:3, HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2

ok

つまりハンドラが何をすればいいかというと、9997ポートから来た上の形式のメッセージをパースしてリクエストを判断し、処理結果を下の形式に加工して9996ポートに送ってやる、とこういうことです。

ミニマムなハンドラを作る

そしてこちらが調理済みのハンドラになります(料理番組風)。

#!/usr/bin/env ruby
# coding: utf-8

require 'zmq'

sid   = "70D107AB-19F5-44AE-A2D0-2326A167D8D7"
con   = ZMQ::Context.new
rsock = con.socket(ZMQ::UPSTREAM)
ssock = con.socket(ZMQ::PUB)

rsock.connect('tcp://127.0.0.1:9997')
ssock.connect('tcp://127.0.0.1:9996')
ssock.setsockopt(ZMQ::IDENTITY, sid)

loop do
  str = rsock.recv
  sender, conn_id, path, str = str.split(' ', 4)
  ssock.send "#{sender} #{conn_id.size}:#{conn_id}, HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nok", 0
end

こいつとMongrel2を起動させといて、http://localhost:6767/m2testにアクセスすると、okとそっけない返事が返ってくる、というだけのハンドラ。「今夜は帰したくない」とか「結婚しよう」とかいうリクエストを投げても「ok」って返してくれます。まぁ「別れよう」でも「ok」って返ってきますけど。

上のコードだとreceiveしたメッセージのセンダIDとコネクションIDしか見てないけど、ヘッダとボディをパースすればRackアプリに渡すENVを作ることができるし、Rackアプリが返すレスポンスの仕様は決まってるのでそれをMongrel用のレスポンスメッセージに変換してやるのも簡単にできるわけで、そこまですればMongrel2と連携できるRackハンドラが作れる、というわけ。あとは、このハンドラ自身でリクエストを処理しなくても、スレッドを一杯作ってその中でRackアプリの処理をさせて、ハンドラ自身はプロセス内通信でリクエスト/レスポンスの中継役になってやるとかすれば、ワーカをがんがん増やせるとか、そんな風にもできる。

あと、Mongrel2とハンドラ間はZeroMQで通信してるだけなので、お互いが生きてるか死んでるか、何個あるのか一個もないのか、などについて何も感知しない。ので、急に負荷が上がったらプロセスやサーバを増やして緊急投入してもMongrel側の設定は変更する必要なかったりとか、デプロイ時にはもう一個ハンドラプロセスを立ち上げて起動し終えたら古いプロセスを殺すとかしてやればダウンタイム無しでデプロイできたりとか、するんじゃないのかな。多分。

あ、ちなみに今回作ったものはGistに置いといた。

「RubyKaigiが終わったら真面目にやろう」とか言ってたくせに、中々やる暇無くて放置してたら大変に分かりやすい作り方講座が出てしまった上、弾さんまで乗ってきてしまって完全にタイミングを逃したfaultierですこんばんは。

悔しいので対抗してみる

うどんげが出たならてゐもいてもいいだろう、ということでてゐ。AAは上記の記事同様こちらを使わせてもらった。初春もいいなーと思ったんだけど表示してみたら大き過ぎて自分のターミナルで表示できなかったのでやめといた。コードはこんな感じ。

# tewi.rb
eval$s=%w't=true;e="eval($s=join("<<34<<34<<",qw{$t=1;$s=~/"<<92<<"[[0-9,]+"<<92<
<"]/;$n=eval($&);$e=          "<<39<<"eval$s=%w"<<39<<    ".chr(39)."<<39<<($s+(
($s.length>1756)?"   ":"#"<<$    s.gsub(/[^0-9a-zA-   Z]/,      "")[0,(1755-$s.l
ength)]));e[-312,  311]=""<<39<<   ";$e.=          ("<<39<<"#"<   <39<<".substr(
join("<<39<<39<<  ",split(/[^0-9a-z              A-Z]/,$s)),0,(200  9-length($s)
))).chr(39)."<<  39<<".join"<<39<<";@o   =       map{$t=!$t;split(//  ,((!$t)?su
bstr($e,0,$_,"< <39<<39<<"):chr(32)x$_)         )}@$n;for(1..34){spli  ce(@o,$_*
81,0,chr(10))}  ;print(join("<<39<<39<<         ",@o).chr(10      )    )}))";o=[
101,10,24,4,40 ,3,8,4,18,3,4,6,33,2,13           ,3,6,10,1            2,3,30,2,1
7,14,17,2,27,  2,21,3,1,7,19    ,2                     ,              25,1,23,9,
21,2,23,2,23,  9,12,6,1,                           4                   ,23,1,23,
11,9,12,23,2, 13,4,2,2                   1                             ,1,14,23,
2,9,27,1,19   ,22,1,8         ,                  1    9   ,             1,29,20,
3,7,9,1,      18,1,4      ,         1        ,          3           ,    1,13,16
,6,6,6,      1,9,1,8                          ,1           ,    1    0,    1,11,
1,4,14,      6,7,26,                   2,1     1,1,4   ,1              ,4,   2,4
,12,6,7     ,19,3,5,             5,  3,2,14,3  ,3,1   0,5               ,8,13  ,
2,2,8,    2,4,3,3,15,          5,2    ,7    ,  4,11 ,10,3,              4,2,4,1,
2,4,    1,6,14,12,4,1      3  ,   6,1,     2,1,3,4,5,18,3,1,3   ,   1   ,3,12,3,
12,1   3,5,1,2,4,15             ,1,1, 11    ,10,3,14,12,6,1 ,           2,5,13,2
,1   ,1,1,6,12,2,16            ,11,7, 8,     13,2,1,7,13,1  , 1      8,10,8,6,14
,  1,2,10,10,1,20,1           2,5,3,1        2,1,6,13,7,2,  1       9,13,26,14,7
, 2,16,16,11,1,13,14          ,8,2,14,      16,13,7,5,15,1 0,          3,8,10,1,
8 ,25,10,1,4,13,27,23,            12,4,   2,12,5,1,1,1 ,8,1,1             3,17,1
0  ,1,3,5,1,12,14,4,8,             4,10,2,13,25,2,3,9,4,2,1,3              ,15,1
3,  33,4,5,1,4,3,20,                8,182].map{ |i|t=!t;((!t)              ?e.sl
ice  !(0,i):32.chr*                i)}.join;1.up       to(35               ){|i|
o[(i*   81)-1,0]          =        10.chr};puts(o)#ttrueeeva          l    sjoin
3434qwt1                           s920992nevale39evalsw39            chr3  939s
slength1     7 5        6             sgsub09azAZ01755s          l   ength e3123
1139e39              39su        bstr          jo             #t1s09nevaleevalsw
chr39tt  rue         eeva  l   sjoin3434qwt1s9             20992nevale39evalsw39
chr3939sslen    gth17 56sg   sub09azAZ01755slengt        he31231139e3939substrjo
in3939split09azAZs02009lengthschr3939join39omapttsplittsubstre03939chr32xnfor134
spliceo810chr10printjoin3939ochr10o10110244403841834633213361012330217141'.join

Gistにも置いてあります。はい。

Quineじゃない件

上のコードをコピペしてRubyに実行させると何やらコードを吐くので、それをさらにRubyに流し込んで実行させてやると…なんということでしょう!エラーを吐くではありませんか!Quineになってねぇじゃねーか、このド低能が!

# tewi.pl(tewi.rbの出力結果)
eval($s=join("",qw{$t=1;$s=~/\[[0-9,]+\]/;$n=eval($&);$e='eval$s=%w'.chr(39).'t=
true;e="eval($s=join(          "<<34<<34<<",qw{$t=1;$s=    ~/"<<92<<"[[0-9,]+"<<
92<<"]/;$n=eval($&)   ;$e="<<3    9<<"eval$s=%w"<<39   <<".      chr(39)."<<39<<
($s+(($s.length>17  56)?"":"#"<<$   s.gsub          (/[^0-9a-zA-   Z]/,"")[0,(17
55-$s.length)]));  e[-312,311]=""<<3              9<<";$e.=("<<39<<  "#"<<39<<".
substr(join("<<3  9<<39<<",split(/[^0-9   a       -zA-Z]/,$s)),0,(200  9-length(
$s)))).chr(39)." <<39<<".join"<<39<<";@o         =map{$t=!$t;split(//,  ((!$t)?s
ubstr($e,0,$_,"  <<39<<39<<"):chr(32)x$_         ))}@$n;for(1      .    .34){spl
ice(@o,$_*81,0, chr(10))};print(join("<           <39<<39<<            ",@o).chr
(10))}))";o=[1  01,10,24,4,40    ,3                     ,              8,4,18,3,
4,6,33,2,13,3,  6,10,12,3                           ,                   30,2,17,
14,17,2,27,2,2 1,3,1,7,                   1                             9,2,25,1
,23,9,21,2,2   3,2,23,         9                  ,    1   2             ,6,1,4,
23,1,23,1      1,9,12      ,         2        3          ,           2    ,13,4,
2,21,1,1      4,23,2,                          9,           2    7    ,1    ,19,
22,1,8,1      9,1,29,                   20,     3,7,9   ,1              ,18   ,1
,4,1,3,1     ,13,16,6             ,6  ,6,1,9,1  ,8,1   ,10               ,1,11  
,1,4,14    ,6,7,26,2,1          1,1    ,4    ,  1,4, 2,4,12              ,6,7,19
,3,5,    5,3,2,14,3,3,      1  0   ,5,8     ,13,2,2,8,2,4,3,3,   1   5   ,5,2,7,
4,11,   10,3,4,2,4,1             ,2,4, 1,    6,14,12,4,13,6, 1           ,2,1,3,
4,5   ,18,3,1,3,1,3,            12,3,1 2,     13,5,1,2,4,15  , 1      ,1,11,10,3
,1  4,12,6,1,2,5,13,           2,1,1,1        ,6,12,2,16,11  ,       7,8,13,2,1,
7, 13,1,18,10,8,6,14,          1,2,10,1      0,1,20,12,5,3, 12          ,1,6,13,
7, 2,19,13,26,14,7,2,16            ,16,1   1,1,13,14,8, 2,14,1             6,13,
7,  5,15,10,3,8,10,1,8,             25,10,1,4,13,27,23,12,4,2,              12,5
,1,  1,1,8,1,13,17,10                ,1,3,5,1,12 ,14,4,8,4,10,              2,13
,25,  2,3,9,4,2,1,3,                15,13,33,4,5,       1,4,3               ,20,
8,182]   .map{|i|          t        =!t;((!t)?e.slice!(0,i):3          2    .chr
*i)}.join                           ;1.upto(35){|i|o[(i*81)            -1,0  ]=1
0.chr};pu     t s        (             o)#ttrueeevalsjoi          n   3434q wt1s
920992ne              vale        39ev          al             sw39chr3939ssleng
th1756sg  sub         09az  A   Z01755slengthe3             1231139e3939substrjo
';$e.=('#'.su    bstr( join   ('',split(/[^0-9a-zA        -Z]/,$s)),0,(2009-leng
th($s)))).chr(39).'.join';@o=map{$t=!$t;split(//,((!$t)?substr($e,0,$_,''):chr(3
2)x$_))}@$n;for(1..34){splice(@o,$_*81,0,chr(10))};print(join('',@o).chr(10))}))

ええはい。良く見てもらえると分かると思うけど、実はこのコードは自分自身を出力してない。何を出力してるかと言うと、Perlのコードを出力している。んで、そのPerlのコードは何をするかというと、最初のRubyのコードを出力する。つまり、最初のコードは「『このRubyのコードを出力するPerlのコード』を出力するRubyのコード」で、後のコードは「『このPerlのコードを出力するRubyのコード』を出力するPerl」のコードになっていて、お互いがお互いの自分自身を相手に出力させるという、「人は一人では生きて行けないんだ」というメッセージ性を持った難読コードなわけです。まぁ嘘です。RubyもPerlもやられちゃったので、そのまま真似してもつまらなかっただけです。こんな感じで遊んでみるといいよ!

$ cat tewi.rb | ruby | perl | ruby | perl | ruby

何をしたの?

実のところ別になんということはなくて、やってることは基本的にはうどんげQuineと一緒。配列リテラルとjoinとevalを使ったQuineはPerlでもRubyでも全く同じ要領でできるので、自分自身を吐く代わりに相手のコードを吐くように書き換えると簡単に行ったり来たりできるようになる。

# 任意に加工できるRubyコードを出力する、
# 任意に加工できるPerlコードを出力する、
# 任意に加工できるRubyコードの例
eval$s=%w'puts("eval(join("<<39<<39<<",qw{print("<
<39<<"eval$s=%w"<<
39<<".chr(39)."<<39<
<$s<<39<<".chr(39)."<<39<<".join"<<39<
<")}))")'.join

あとは、二言語分のコードが一つのAAの中に入ることになるし、PerlとRubyだとリテラルがそっくりなのでデータ部は共有したいので生のデータを書きたい、となると流石に元のAAデータだと文字数の制限がキツいから、反転して白抜きのAAにした。ちなみに、てゐの方はうどんげのとはAAデータ持ち方は換えてる(元記事のロジックをPerlで実装するのが面倒だっただけ)。

あとはまぁ、出力時に尻尾に適当なゴミを付けて文字数調整してるんだけど、文字列リテラルの入れ子が簡単に崩れるので面倒臭いとかそんな程度。chr(39)濫用しまくり。あとドットがRubyのドットなのかPerlのドットなのか良くわかんなくなったりします。

まとめ

途中で「俺は一体何をやってるんだ」と思ったら多分負けなんだと思います。一度ベースができちゃうと後はデバッグと文字数の調整が面倒なだけで、割と簡単にできます。あと段々哲学的な気分になります。

まぁなんだ、本来なら書くべきことは一杯あるんだけども、眠くてまとめてる時間が無かったので、これだけ書いておきますね。

eval(%w(eval(a="eval(%w(b=[35,35,35,
32,34,69   ,11   5,1   11,116,101,1,
14,105,9   9,  32,79   ,98,102,117,
115,99,9      7,116,   101,100,32,82
,117,98,   121,32,80   ,1        14,11
1,103,11   4,97,109,      109,1   05,
110,103,   34,32,105     ,115,3   2,103,
11   4,1   01,97,116   ,  33];p   uts
(b   .pa   ck('C*'))   ).        join)
;printf(\"eval(a=%p)\n\",a)")).join)

うん、あれだね、中途半端なのはよーく分かってるんだ。日本語で出したかったしちゃんと2回目以降も整形されたコード出すべきなんだけど、えーと、RubyKaigiが終わったら真面目にやろうとは思うんだ。衝撃を受けたって気持ちだけでも伝わるといいな。

そろそろRuby会議もあるというのに、そういや最近Ruby全然書いてないfaultierですこんばんわ。じゃあリハビリを兼ねて久々にRamazeさんで遊んでみるか、ついでにいい加減NoSQLブームにも乗ってみるか、みたいな感じでRamaze+MongoDB+Candyで遊んでみることにした。ちなみに社内では今Cassandraがブームなのだけども、Cassandraはちょっと遊びで使ってみるにはオーバースペックだよなーとか思いつつ色々見てたら、HerokuでMongoDBが使えるらしいのでそれを狙ってのMongoDBいじり。

とりあえず入れてみる

aptで探したらmongodbのパッケージもあるんだけど、2010年5月30日時点では1.2.2と若干バージョンが古い。この後オブジェクトマッパーを色々試してみたところ1.4系じゃないと動かなかったりしたので、本家のサイトから最新のバージョン落としてくる。コンパイル済みのパッケージなので展開して適当なとこに置いとくだけでOK。手元の環境はUbuntu 10.04の64bitなのでLinux 64bitってやつを選んだけど、OSX 32/64 bitとかWindows 32/64 bitとかもあった。

$ mongo
MongoDB shell version: 1.4.3
url: test
connecting to: test
type "help" for help
>

ふむ。繋った繋った。このあとチュートリアルとかにある例通りちょこちょこいじってみたけどちゃんと使えるようだ。

Rubyから使う

Rubyのdriverはgemからインストールできるので普通にgem install mongoとかやる。依存モジュールでbsonてのが入るけどなんじゃろ、と思ったんだけど、RubyのオブジェクトをMongoDBのBSONオブジェクトにシリアライズするモジュールだけ別のパッケージにしてあるらしい。んで、bsonはC拡張があって、そっちを入れてないとこんなメッセージが出る。

rb(main):001:0> require 'mongo'

**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance.
  You can install the extension as follows:
  gem install bson_ext

  If you continue to receive this message after installing, make sure that the
  bson_ext gem is in your load path and that the bson_ext and mongo gems are of the same version.

=> true

まぁローカルの開発環境だと別にどっちでもいいんだけど、サーバに置くときはどうせ使うだろうしbson_extも入れておく。

# coding: utf-8
require 'mongo'

con   = Mongo::Connection.new # 何もオプション指定しないと localhostの28017ポートに繋ぐ
db    = con.db('candy')
udons = db.collection('Udon')
udons.insert({
  'name'  => 'ぶっかけ',
  'type'  => '冷たいうどん',
  'price' => 350
})
udons.insert({
  'name'  => 'かけ',
  'type'  => '温かいうどん',
  'price' => 300
})
udons.insert({
  'name'  => '釜玉',
  'type'  => 'かまあげうどん',
  'price' => 400
})

p db.collection_names  #=> [ 'system.indexes', 'Udon' ]
p udons.count          #=> 3
p udons.find_one       #=> {"_id"=>{"$oid"=>"4c0273712e119e3fd7000001"}, "name"=>"ぶっかけ", "type"=>"冷たいうどん", "price"=>350}
p udons.find('price' => {'$gte' => 350}).count #=> 2

こんな感じでさくっと使えた。ほー。あとはMapReduceとか試してみようかと思ったけどまぁそれはおいおい。

オブジェクトマッパー

ぶっちゃけmongo-ruby-driverだけでも十分色々できるし、O/Rマッパー…あーいやRelationじゃないから、O/Dマッパーか、は無くてもいいかなと思ったけども、Ramazeとかで使うことを考えたらやっぱりあった方がいい(工夫してやれば既存のヘルパーとか使えそうなので)。んでちょろっと見てみたら、結構色々あるんだけど、どれもARっぽいんだよなぁ。なんというか、ちょっと過剰なのでもっと薄いのがいい。そしてMongoMapperもMongoidもActiveSupportとかvalidation用のライブラリとか入れちゃう。Railsで使うんならいいんだけど、というかRailsでARの代わりに使うのを想定してるっぽくて(ActiveModelってのがあるんだね。最近Rails全然見てないので知らないんだけどRailsもModel抽象化の流れなんかな)、悩ましい。つーかActiveSupportは入れないで欲しいなぁ…。

個人的には一覧の最後にあったCandyが良さげに見えた。なにしろ「ゴールはActiveRecordやDataMapperのミラーじゃない」ってREADME冒頭で明言してるし、mongo-ruby-driver以外には依存してないのが素敵。使い方もCandy::Pieceをincludeするだけとシンプル。

# coding: utf-8
require 'candy'

class Udon
  include Candy::Piece
end

kamaage = Udon.new
kamaage.name = 'かまあげ'
kamaage.type = 'かまあげうどん'
kamaage.price = 350

ちなみに、何も指定しなければこれでlocalhostの28017ポートに繋いで、candyというデータベースのUdonコレクションにアクセスする。つまりこのコードだと、一個前のコードと同じデータベースの同じコレクションから取ってくることになる。これはCandyクラスかUdonクラスのクラスメソッドで参照/変更できる。Candyクラスで設定すると全てのデフォルト設定になるし、クラスメソッドで設定するとそのクラスのデフォルト設定になる。Ramazeだったら、model/init.rbあたりでこう書いとく。

# coding: utf-8

require 'candy'

Candy.host = "mongdb.example.com"
Candy.port = "24423"
Candy.db   = "udonapp"

require __DIR__('udon')

Udon.collection = 'menrui' # クラス名じゃないコレクション名にしたい場合

あとsaveメソッドはない。なんかこれもポリシーらしくて、オブジェクトを操作したら即時反映される。method_missingでフックして随時更新をかけてるので、もしvalidationとかしたい場合は、例えばpriceが数字でなければならなくて、1000円超えるようなお高いうどんは売っちゃいけないポリシーにするとしたら、こんな感じのメソッドを生やしてやる。superが肝。

class Udon
  include Candy::Piece

  def price=(val)
    raise '値段は数字だっつってんだろ' unless val.kind_of?(Integer)
    raise 'たけーよアホか' unless val < 1000
    super
  end
end

ちなみにバリデーションとかエラーハンドリングとかnamed scopeとかの簡単な実装例はCandyのドキュメントに載ってるので見てみるといいと思う。まぁ若干面倒ではあるけど、正直こんなのでいいよなーと思った。しばらくこれで遊んでみよう。

…というあたりまで書いて大変致命的なことに気付いたんだけど、Candyは1.9系じゃないと駄目らしいんだけどHerokuってRuby 1.9系だっけ…?多分違うよね…?うーん、まぁ、ちょうど借りてるサーバもリビルドしたことだし、とりあえずそのサーバで動かせればいいか…。

こんにちは、「それは一体誰得なんだ」でお馴染みのfaultierお兄さんだよ!今日はみんな大好きMacRubyをどれだけ無駄遣いできるかを考えてて例のごとく失敗したので、その顛末を教えてあげるよ!

MacRubyでDTを動かしたい

まぁ冒頭書いた通りなんだけど、「Objective-CからMacRubyを利用する - Watsonのメモ」を読んでなんか変なことできないかなーと考えてて、そういや俺ってば見た目に面白い以外は全く使い道のないものを以前作ってたじゃん、と思い出したんだけど、上手くいかなったという話。あ、全く使い道の無いものってのは、もちろん言うまでもなくあいつのことですね。

esotericは構成としては、ソースコードをパースしてSexpにするParserと、それをRuby2Rubyを使ってRubyのコードにトランスレートしてから実行するRunnerでできているので、MacRuby Frameworkを使ってesotericをObjCから呼び出せば、アプリケーションにDTやてってってーでプラグインを書ける仕組みを比較的容易に導入できるかと思います。導入したところで誰が使うのかわかりませんが。少なくとも俺は絶対に使わない。

まずは小手調べのコンパイルエラー

とりあえずMacRubyをDownloadしてくる。最新版の0.5はSnow Leopardにしかインストールできないけどこないだクリーンインストールしたばっかりだから全然問題ないもんね!と勝ち誇ってみせたけど、一体誰に勝ったのかはよくわからない。ちなみにソースからのビルドも時々試みてるけど大体こけるので今回は無難にバイナリをインストール。macgemは0.4のときはまともに使えたもんじゃなかったのでちょっと不安だったけど、Ruby2Rubyも特に問題なく入った様子。なに、こんな拍子抜けするくらいさらっと入っちゃっていいの?とニヤニヤしながら次のコマンドを実行。

$ pwd
/Users/taro/Projects/esoteric
$ echo $RUBYLIB
lib:
$ macruby -v 
MacRuby version 0.5 (ruby 1.9.0) [universal-darwin10.0, x86_64]
$ macruby bin/dt -v
/Users/taro/Projects/esoteric/lib/esoteric/dt/parser.rb:13: end pattern with unmatched parenthesis: /((?:\xE3\x81\xA9|\xE7\xAB\xA5\xE8\xB2\x9E\xE3\x81\xA1\xE3/
/Users/taro/Projects/esoteric/lib/esoteric/dt/parser.rb:74: end pattern with unmatched parenthesis: /(\xE3\x81\xA9|\xE7\xAB\xA5\xE8\xB2\x9E\xE3\x81\xA1/
dt.rb:3:in `<main>': compile error (SyntaxError)
    from dt:4:in `<main>'

オゥフ。言われたところを見てみたら、parser.rbの13行目には/((?:ど|童貞ちゃうわっ!)+)…/という正規表現が書いてあった。念のため試してみたけど、Ruby 1.9.1ではちゃんと動いてる。どうも、()の中にASCII以外の文字が含まれてるとMacRubyさんは閉括弧を見つけられなくて正規表現として不正だと言ってくる様子。ソースコードはutf-8で書いてあって、magic commentにもutf-8って指定してて、文字列リテラルだと問題ないのに、正規表現だと駄目。仕方ないのでベタに日本語書いてたところを全部Unicodeリテラルにしてみた。"ど"だったら"\u3069"とか。とりあえずそれでコンパイルできないというエラーは出なくなった。CRubyの方でももちろんちゃんと動く。なんだよ、やればできるんじゃないか、ツンデレか?などと思いつつhi.dtを実行させてみる。

$ macruby bin/dt -v
esoteric 0.0.2, DT 0.0.2
$ macruby bin/dt examples/hi.dt
parser.rb:160:in `numeric:': ArgumentError (ArgumentError)
from parser.rb:80:in `process'
from parser.rb:58:in `block'
from parser.rb:51:in `parse'
from parser.rb:11:in `parse:'
from runner.rb:25:in `run:'

ぬぅ。まだツンツンしてやがる。ちょっと勢い込んでしまったけど、どうもまだMacRubyと打ち解けきれてないみたい。ちなみに、CRubyの方でやるとこんな感じになる。

$ ruby -v
ruby 1.9.1p243 (2009-07-16 revision 24175) [i386-darwin10.0.0]
$ ruby bin/dt -v
esoteric 0.0.2, DT 0.0.2
$ ruby bin/dt examples/hi.dt
$stack = []
$heap = {  }
$stack.push(72)
$stdout.print($stack.pop.chr)
$stack.push(105)
$stdout.print($stack.pop.chr)
$stack.push(33)
$stdout.print($stack.pop.chr)
$stack.push(10)
$stdout.print($stack.pop.chr)
exit(0)
Hi!

うーん、ちゃんと動いてるよなぁ。該当の箇所を調べたら、本来encodingがUTF-8のStringが来てなきゃいけないところで、MacRubyの場合はUS-ASCIIなStringが来てしまっている。あるぇ?その文字列がどっから来てるかを辿って行くとARGF.readしてるとこなんだけど、MacRubyでは既にその時点でUS-ASCIIとして読み込んでしまっている。CRubyでやったらちゃんと動くのだから、$stdin.external_encodingはちゃんとUTF-8になるはずなんだけど、そもそもそこがnilだし、opneとかset_encodingとかで指定しても変化なし。force_encodingとかしても上手くいかない。と、このあたりでもっと色々なことがおかしいということに気付く。

MacRubyでStringが期待した挙動をしてない件

いまいち良くわからないので、試しにこんなことをしてみた。

$ cat test_string.rb
# coding: utf-8
a = "ど"
b = "\u3069"
puts "\"ど\".encoding        #=> #{a.encoding}"
puts "\"\\u3069\".encoding    #=> #{b.encoding}"
puts "\"ど\" == \"\\u3069\"     #=> #{a == b}"
puts "\"\\u3069\" == \"\\u0069\" #=> #{b == "\u0069"}"
puts "\"ど\" =~ /\\u3069/     #=> #{a =~ /\u3069/}"
puts "\"i\" =~ /\\u3069/      #=> #{"i" =~ /\u3069/}"
puts "\"i\" =~ /\\u0069/      #=> #{"i" =~ /\u0069/}"
$ ruby testb_string.rb
"ど".encoding        #=> UTF-8
"\u3069".encoding    #=> UTF-8
"ど" == "\u3069"     #=> true
"\u3069" == "\u0069" #=> false
"ど" =~ /\u3069/     #=> 0
"i" =~ /\u3069/      #=> 
"i" =~ /\u0069/      #=> 0
$ macruby test_string.rb
"ど".encoding        #=> UTF-16
"\u3069".encoding    #=> US-ASCII
"ど" == "\u3069"     #=> false
"\u3069" == "\u0069" #=> true
"ど" =~ /\u3069/     #=> 
"i" =~ /\u3069/      #=> 
"i" =~ /\u0069/      #=> 0

おぉう…どういうことなの…なんでこんなに違うの…。ちゃんとわかってないんだけど、こんな感じなのかしら。

  • MacRubyはソースコードがUTF-8で書かれているものと想定して、それをUTF-16に変換している?あと、magic commentを見てないようで、試しにeuc-jpで書いてみたらバイト列をそのままUTF-16の文字列だと解釈してStringクラスにしていて、化ける。
  • IOからの読み込みはASCIIとして扱っている。ARGFでもopenでも同じだった。こちらも環境変数、コードのencoding、magic comment、読まれるファイルのencodingに関わらず同じ。
  • String#encodeやString#force_encodingが何もしないでselfを返してるように見える。NSStringのメソッドを使って変換してやれば変わるんだろうか?
  • Unicodeリテラルを解釈するときに、\uXXXXの後ろ二桁しか見てないっぽい。"\u3069" == "\u0069"がtrueって何の冗談かと思った。
  • Unicodeリテラルの扱いが、文字列リテラルの中なのか正規表現リテラルの中なのかで違っている。"i" == "\u3069"はtrueだけど"i" =~ /\u3069/はfalse。そう言えば、"(ど)"は正しくパースできるのに/(ど)/はSyntaxErrorになるところを見ると、Unicodeリテラルに限らずそもそもそこのパースのロジックが微妙に統一されてない感じ。

さてどうしたもんかな…。日本語を正規表現でマッチさせてるところがまずい(ちなみにBrainf*ckは完全に、Whitespaceは不完全ではあるけど一応動いたので、ソースコードと入力にNON-ASCIIな文字列が含まれてなければ問題ないらしい)なら、完全にバイト列だと思って扱ってやるとか、正規表現じゃなくて==しか使わないとか(もちろんバイト単位で比較)、ObjCでまず入力を正規化してやった上でMacRubyに渡すとか(本末転倒!)、そういう風にすれば動かないでもないかもしんないけど、そういう文字列処理みたいなObjCであんまり書きたくないところをRubyでさらっと書けるから良いんであって、それ以外のところはそもそもObjCで書いたって大して難しくない。performSelectorとかランタイムAPIとか使いまくればいいんだよ!というわけでちょっと残念な感じ。

余談

esotericに付属のesocコマンドを使うとDTやBrainf*ckのコードをRubyのコードに変換できるので、出来たコードをmacrubycにかけてやれば最終的にMacOSXで動作するバイナリができます。DTのコードがなんと高速で動作するネイティブのバイナリに!…と思ったけど結構遅かった。なんかこう、色々読み込むののオーバーヘッドが馬鹿にならない感じ。でも、普通にRubyを使うとstack level too deepで動かないような深い再帰のコードでも動いたりする。ていうかexamples/fact.*、macrubycでコンパイルしないと動かないんですけど。何でこんなコード入れてんだ俺。

最近ブログ更新してないなーと思ってふと最終更新日見たら3ヶ月も前の日付になっていた。良くない。ブログ書いてないということはブログに書けるような馬鹿なことをしてないということで、3ヶ月間一日も休まず馬鹿であったにもかかわらず馬鹿なことをしてないというのはとても良くない。これはアイデンティティの危機である。

ということで久々に馬鹿なことをしないとと思い立ったわけでは全然なくて、ただただ単純に八九寺が可愛過ぎるので、八九寺に名前を噛みまみたしてもらえるモジュールを作ってみた。結果的にだいぶ馬鹿になった。

faultier's kamimamize at master - GitHub

こんな風にして使います。

$ cat kamimamizer.rb
#!/usr/bin/env ruby
# coding: utf-8
require 'kamimamize'

mayoi = Kamimamize::Coverter.new({
    :pattern => { :priority => 10 },
    :repeat  => { :priority => 5, :through => 0.4 },
    :swap    => { :through => 0.05, :appid => 'APPID' },
    :default => { :class => 'Kamimamize::Plugin::Random' }
})

puts mayoi.kamimamize('阿良々木', 'あららぎ')
puts mayoi.kamimamize('羽川', 'はねかわ')

$ ./kamimamizer.rb
阿良々々々木さん
骨川さん (ほねかわさん)

名前とその読みを入れるとカミママイズされた名前が出てきます。カミママイズだなんていやらしいです。それだけです。それだけですが、何か?

一応ちょっとだけ解説すると、引数にプラグインの設定を入れてKamimamize::Converterをnewすると、kamimamizeするときにそのプラグインのどれかを使って名前を変換します。priorityが高い奴から順に処理されて、自分が処理できない名前のときは次のプラグインに任せます。throughが設定されてると大体それぐらいの割合で答えられるときでもスルーして次に渡します。上の例だとrepeatプラグインは4割くらいの確率で処理できるときでもスルーします。全部のプラグインがスルーすると最後にdefaultのプラグインが拾います。何も設定されてないとrandomプラグインがdefaultで、ランダムで八九寺にひどいことを言われます。

プラグインはとりあえず4つ作ってあって、

  • HashかYAMLファイルのパスで名前とそれに対応する変換結果のリストを渡すと、対応する名前が来たとき変換結果リストの中からランダムで返すpatternプラグイン。何もリストを渡さないと「八九寺」にだけ反応する。
  • 同じ文字が続く名前、もしくは「々」が含まれる名前のときにその数を増やしたり減らしたりして噛むrepeatプラグイン。
  • 名前の読みの母音や子音を適当に置き換えるswapプラグイン。Yahoo!のテキスト解析APIを使うのでアプリケーションIDが必要。ちなみに、名前の字面を見ているわけではなく読みを適当に置き換えるだけなので、そんなに上手いことは言わない。
  • 返答リストからランダムに返すだけのrandomプラグイン。返答リストは初期化時に設定できるけど、しないと八九寺にひどいことを言われる。Converterをnewするときに明示的にdefaultを設定しない場合、このプラグインがthrough=0で一番後ろに設定される(つまり上の例ではdefaultの設定は意味ない)。

がある。上の例だと阿良々々々木さんはrepeatプラグインで、骨川さんはswapプラグインの変換結果。何かと適当なので多分そんなに面白い結果は出ないと思うけど(例えば戦場が原とか入れると意味不明すぎて噛んだのかどうかすら怪しいのが出てくるとか、阿良々木さんをカミママイズすると木が消えちゃうとか)、個人的には羽川が骨川とか花沢に変換されたのが面白かったので満足です。ひらがなの音を置き換えるのって結構面倒なのよ。というか八九寺の神懸ったセンスに勝てる気がしない。

あとは類義語に置き換えるとか、名前の中の語彙の分割を真面目にやるとか、specくらい書くとかしたいけど、多分飽きてやんないだろう気配がします。自分で言うな。なんというか、カミママイズするためだけにY!のAPIを使うという無駄遣いをしたかっただけだったりする。Y!のAPI面白かった。途中で形態素解析とかして遊んでた。どうせならもっと盛大に無駄遣いしてみたいなあ。

ねんどろいどぷち 化物語セット 其ノ貮 (ノンスケールABS&amp;PVC塗装済み可動フィギュア)
ねんどろいどぷち 化物語セット 其ノ貮 (ノンスケールABS&amp;PVC塗装済み可動フィギュア)
クチコミを見る
ねんどろいどぷち 化物語セット 其ノ壹 (ノンスケールABS&amp;PVC塗装済み可動フィギュア)
ねんどろいどぷち 化物語セット 其ノ壹 (ノンスケールABS&amp;PVC塗装済み可動フィギュア)
クチコミを見る

余談

上のねんぷちの化物語セット、これを書いてる時点ではAmazonでは受付終了しちゃってるんですが、それを今更気付いてものすごく落ち込んでたら、twitterで

と八九寺が教えてくれるという素敵展開が。@mayoi_hachikujiには足を向けて寝れそうにないと思いながらどこにいるか知らないので普通に寝ました。ありがとう八九寺。大好きだ八九寺。

どうやらUnicornというのが良いらしいという噂を聞きつけたので、どんなもんじゃろと試してみることにした。

Route 477 - 大規模Railsサイトのための新しいHTTPサーバ、Unicorn

Unicornてのは何者なのかと言うと、Rack及びRailsに対応したRubyのWebアプリ用のHTTPサーバ。詳しくは上の記事を読んで下さい。githubでも使ってるそうだ。あと、名前が格好良い(あんまり関係ない)。

まずはunicornの設定

と言っても、gem install unicornしてconfig.ruがあるディレクトリでunicornコマンドを叩けば、thinとかと同じようにサーバが起動する。rackup互換のオプションも付いてるので特に悩むこともないと思う。あとは普通にApacheとかでプロキシの設定してやるなりなんなりすればすぐ使える。

それだけだと大して面白くないので、折角だから上の記事に書いてあるように、nginxでソケットでっていう設定をしてみる。先にUnicornの方の設定。ちなみにサンプルに使ったのは例によって彼女が404のやつ。今現在はApache2+Passengerで動いている

# unicron.conf
worker_processes  4
working_directory '/var/www/rackapp'
listen '/tmp/rackapp.sock', :backlog => 1
listen 4423, :tcp_nopush => true
timeout 10
pid '/tmp/rackapp.pid'
preload_app  true
stderr_path '/var/log/rackapp.log'

before_fork do |server, worker|
  old_pid = "#{server.config[:pid]}.oldbin"
  if old_pid != server.pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end

  sleep 1
end

after_fork do |server, worker|
  addr = "127.0.0.1:#{4423 + worker.nr}"
  server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => 1)
end

中身はほとんどUnicornの設定例からコピペ。ちなみに、nginxを推奨してるだけあってnginxの設定に似せたDSLになってるけど、実態は見ての通りRuby。その気になれば動的に設定をごにょごにょみたいなこともできる。ログはThinみたいによしなにやってはくれないけど、その辺は設定でLoggerを渡すなりRackのミドルウェアを使ってなんとかするなりすればOK。

んでこれを適当なファイル名で保存したら、次のようにunicornコマンドを叩く。

$ unicorn -D -c /var/www/rackapp/unicorn.conf /var/www/rackapp/config.ru

-Dオプションでデーモン起動、-cオプションは設定ファイルの指定、最後の引数はrackupファイルのフルパス。ちなみにこれを/var/www/rackappじゃないところで実行してみたんだけど、最初は「working_directoryとか指定してるから、そこにあるconfig.ruを勝手に読んでくれたりしないかな」とか思ってたんだけどやっぱりダメで、その後相対パスで指定してみたらrequireでコケて「そんなファイルねーよ、寝言は寝て言え」と怒り出したので、絶対パスにしたら行けた。

この時点でhttp://localhost:4423/がちゃんと見られることと、unicorn.sockが出来てることを確認。よし次はnginx。

nginxのプロキシ設定

こんな感じ。

# nginx.unicorn.conf
upstream rackapp {
    server unix:/tmp/rackapp.sock;
}

server {
    listen       80;
    server_name  localhost;

    location / {
        proxy_pass  http://rackapp;
    }
}

これを大本のnginx.confのhttpのブロックのどこかでincludeしてやる。一応言っとくとこれ以外にも普通のnginxの細かい設定はしてるけど、上の例では省略してます。

ちなみに、ソケットでやるように設定してるからこうなってるけど、TCPで良ければproxy_passのところをhttp://localhost:4423とかにしてやっても普通に動く。

とりあえずこれで完成。あとはnginxを起動してやれば、ちゃんと見られる。素敵。

インターフェースが統一されてるって素敵よね

いやぁ、やっぱRackでインターフェースが統一されてるって、良いですよね。アプリ側あんまり弄らなくてもさくっと移行できるわけですし(っていう話をこないだ書いた)。あと、ついでに同じサーバで動いてたHTTP::Engineのアプリも、apache+modperlからnginx+fcgiに移行してみたけど、こっちもちょこっと設定変えたくらいでアプリ側は全然弄ってない。良いなー。こうして新しい環境が出てきたりしたときにはやっぱりRackやなんやらが真価を発揮する。折角なので次はPlackでH::Eのときと同じことをやってみようと検討中。

んで、結局Unicornは良いの?イイの?

さぁ…ベンチ取ってないのでなんとも。取っても「彼女が404」じゃあんまり参考にならないんだよなー。ただ、アプリをロードし終わってからforkするとかnginx側に気付かせない用に再起動できるとかその辺だけでも十分魅力的ではある。あと、再起動は早かった。流石。

表題通り。MacRubyとMacFUSEでファイルシステムを作ってみようとしたんだけど、なんかいまいち上手く行かない。

まずHotCocoaがちゃんと機能してない。hotcocoaコマンドでアプリの雛形を作ってくれて、macrakeすればビルドが走って.appの形にパッケージされるんだけど、できたアプリを起動しても起動するだけで落ちる。これだけだとあまりにも分からなすぎるので、アプリの形にしないでスクリプトのまま起動してみる。

$ hotcocoa sample
$ cd sample
$ macruby -d lib/application.rb
core:in `dump': nil is not a symbol (TypeError)
	from /Library/Frameworks/MacRuby.framework/Versions/0.5/usr/lib/ruby/1.9.0/hotcocoa/mappings/menu.rb:12:in `submenu:'
	from core:in `menu:'
	from /Users/taro/Projects/sample/lib/menu.rb:3:in `application_menu'
	from /Library/Frameworks/MacRuby.framework/Versions/0.5/usr/lib/ruby/1.9.0/hotcocoa/mappings/application.rb:17:in `load_application_menu'
	from /Library/Frameworks/MacRuby.framework/Versions/0.5/usr/lib/ruby/1.9.0/hotcocoa/mappings/application.rb:8:in `handle_block:'
	from core:in `application:'
	from lib/application.rb:in `start'
	from lib/application.rb:1:in `
'

一応hotcocoaのソースを読んでったんだけど、どうやらメニューバーを生成してるところでコケてるっぽい。そしたらってんでlib/manu.rbに書いてあるHotCocoa#application_menuの中身を丸ごとコメントアウトしたところ、一応動いた。

まぁ作ろうとしてるのはFUSEのファイルシステムをマウントするアプリなので、別にメニューも要らないしViewも出さないしいっか、と思って、気持ち悪いけどそれは特に原因追わずそのままにして、前回のコードをアプリケーションとして起動できるように直してやる。んで再びmacrubyで起動させてみると、どうやらちゃんとマウントできるようだ。よしよし、と気を良くしてmacrakeを走らせたらNamakeFS.appはできたものの、こいつをダブルクリックしても起動して直後に落ちる。スクリプトのままだと行けるんだけどアプリにするとダメ。くそっ。

そもそもHotCocoaでアプリを作るのが今回の主目的じゃないので、もういいや調べるのは別な機会に、と放置することにして、今度は素直にデーモンとして動くように作り直す。これは概ね上手くいって、namakefsコマンドを叩けばNamakeFSが/Volumesの下にマウントされて、FinderからNamakeFSをアンマウントしてやればnamakefsのプロセス自体も終了してくれるようにできた。よしよし。

じゃあ次は早速AtomPubで取得した情報を出してみよう、と思ってatomutilをインストールしてみたが、どうにもこれが動かない。そもそもatomutilは1.9系でちゃんと動くのかって言われると正規表現回りでコケたりして結構微妙なんだけど、まぁ中身読んだのでごまかす手段は知っている。でも、MacRubyから使おうとしたらどうもその問題の箇所にまで到達していない。しゃいせ。

落ち着け、こういうときは慌てず騒がず冷静に、「おかしも(おさない、かけない、しゃべらない、もどらない)」の精神が大切だ、と自分に言い聞かせて、とりあえず素のnet/httpとrexml/documentでサービス文書を取得してみる。そしたら何てことだ、もうその時点でダメだよ。REXML::Document.new(res.body)とかしてみたら、そこでもうコケる。ちょ。ドキュメントオブジェクトすら作れん。ここでatomutil版の方のスクリプトを動かしてみたら、出てるエラー一緒でやんの。ちなみにCRubyの1.9.1ではもちろん何の問題もなく動くコード。ちゃんと期待通りの動作をするのは確認済み。ふぁっきん。

ええい、もうあれだ、REXMLが使えないとかちょっと正直どうかと思うが、MacRubyにはまだあれがある!そうさ、NSXMLDocument(※Cocoaのクラス)!察しの良い方はお気付きかと思いますが、わざわざMacRubyで書いてるのにも関わらずMacFUSEやらNSXMLDocumentやらを使ってるせいで、段々中身はRubyっぽい文法のObjective-Cのコードと化して来ている。もうなんかそろそろ本末転倒な感は否めないけども、とりあえずnet/httpでWSSE認証かましてGETしてきたatomのデータをNSXMLDocumentに食わせればなんとか必要な情報は取れるところまでできた。よし、これをさっきのコマンドに組み込めば…!

segmentation fault …だと?!

なんだか良くわかりません。動いてるコード + 動いてるコード = セグフォ。もう疲れたよパトラッシュ…。

結論から言うとMacRubyはぶっちゃけバグとか地雷とかが多すぎて涙が出てくるので、まだこれでどうこうしようとかは無理です。流石、RubyKaigiのセッションで開発者自ら”Crazy"と言わしめただけあるぜ、MacRuby0.5。これみたいにすごいナチュラルにObjCとRubyの融合ができちゃうっていう「可能性」には期待しているのだけども、まだ早いみたいね…。

さてどうしよう。MacRubyに深入りするのはそれはそれで楽しそうではあるんだけど、そんなことしてる内にFUSEのこと忘れちゃいそうだしなぁ。とりあえずObjCで書くかなぁ。

こないだAtomPubとWebDAVの話をしてて、あーそうかAPIをWebDAVとして実装してたらファイルシステムとしてマウントできて面白かったかもなぁ、でも実装面倒なんじゃないのかなどうだろやったことないし、でもvimで記事書いて:wで投稿されてmvしたらカテゴリ変わってrmで記事削除されてとか何それ胸膨らむ、じゃなかった夢膨らむし胸踊るね、なんて考えたところでふと気付いた。あるじゃん、ファイルシステムじゃないものをクライアント側でファイルシステムにしちゃう仕組み。

そうだ、FUSEだ。Filesystem in Userspace。前々からそのうち遊んでみようとは思ってたものの特にネタも思い付かなかったので手をつけてなかったのだけど、BlogFSに丁度良さそうだし、ちょっとやってみることにした。

Rubyでいじる

FUSEってファイルシステム作るくらいだからやっぱCとかC++とかで書くもんなんだろう、とぼんやり思ってたんだけど、そう言えば前にハチロクの卒研発表会でRubyで実装したとか言ってたよなぁ、あとhatenafsはそう言えばPerlだったっけなぁ、と思い出した。なんだ普段使ってるので書けるな。軽くググったらRubyでもFUSEのライブラリがあるらしい。よし。

…makeでコケた。んー。Rubyのfusefsに問題があるのかもしれないしMacFUSEだからそのせいかも知れないしなんかfusefsの情報古いのしかでてこないなーあー面倒くさいや。どうしたもんかね。

僕らのMacRubyさんがいるじゃないか

RubyからFUSEを使う方法をもう少し調べるとか、Perlから使う方法を調べるとか、そっちに進んでも良かったんだけど、もっとお手軽な方法に気が付いた。MacFUSEをインストールするとMacFUSE.frameworkが入る。てことは、とりあえずObjCからは簡単に使える。ObjCで書くのはまぁいいけど、正直AtomPubの扱いとか文字列の処理とか面倒くさすぎる。あれ、ちょっと待て。ObjCのフレームワークがあるってことは…MacRubyから使えるじゃないか。

$ ls /Library/Frameworks
. .. MacFUSE.framework MacRuby.framework UIM.framework
$ macruby -v
MacRuby version 0.5 (ruby 1.9.0) [universal-darwin10.0, x86_64]
$ macirb
irb(main):001:1 > framework 'MacFUSE'
=> true

素敵!愛してる!

とりあえずHelloFS

MacFUSEのサンプルとか、「雑草ブログ Luaでファイルシステムを実装しよう(MacFUSEで)」あたりを参考にとりあえず実装してみた。

これをmacrubyコマンドで実行する。ちなみにSnow Leopard+MacRuby 0.5beta1+MacFUSE 2.0.3でやってるけど、Leopard+MacRuby 0.4でも動くとは思う(未確認)。

$ macruby namakefs.rb &
$ ls /Volumes/NamakeFS
honne.txt
$ cat /Volumes/NamakeFS/honne.txt
寒いので冬眠したい

OK、OK。上出来。ちなみにこれだけでFinder上にもNamakeFSディスクがマウントされて、ちゃんとCocoaアプリとかからでも開ける。思ったより簡単だった。HotCocoaでアプリ化しちゃえば配布できる形にするのも楽だし、ファイルシステムを作るの自体はObjCの作法に則る必要があるけど、中のロジックはRubyで書けるから、RubyのAtomPubのライブラリも使えるしObjCだと本当に面倒な文字列の処理もさくさく実装できる。

MacRubyでやるってのがまぁ難点と言えば難点だけど。正直開発環境としてはまだアレげだし。RubyCocoaでやりゃいいって話ではあるんだけど、ねぇ…まぁ、いっか。

あんまり関係ないけど、Luaが意外とすんなり読めた件。まぁ中でやってることはまんまObjCだからってのもあるけど。ちょっとLuaもいじってみようかしら。

表題通りですが、Ruby Freaks Loungeで記事を書かせていただきました。

Ruby Freaks Lounge:第23回 Rackとは何か

Rackってそもそも何なの、何でそんなの出来たの、みたいな話を簡単に。んー?誰だー、普段はこんなこととかあんなこととかばっかやってる癖に何真面目な記事書いてんだとか言った奴はー。先生怒らないから出て来なさいー。

RackってRailsだとかSinatraだとかに比べるとマニアックな部類(知名度がというよりは使い道が、ね)だと思うんだけど、予想外に反応が良くて嬉しい。技評の方は「Perl界隈でPSGIも盛り上がってるし、今そういう話題はタイミングが良かったのかも」と言ってたけど、本当にそうだなー。しれっとPSGIに触れたのはまたお前はRubyのことばっかで少しはPerlの勉強しろよ言われないため僕がPerl大好きだからですよ。ホント。ウソジャナイヨ。

ちなみに再来週になったら後編が掲載されてるはず。Rackのミドルウェアの話とかを予定してる(予定ってことはつまりまぁまだ書き上げてないんだけども…)ので、お楽しみに。

なんでもターミナルで完結させたい病

最近Objective-Cをお仕事で書いてたりするんだけど、試行錯誤しながらじゃなくてある程度量書くようになってくるとXcodeじゃかったる過ぎるので、ObjC対応済みのctagsをインストールしたりxcodebuildをよしなに叩いてくれるRakefile書いたりしてvimで作業する環境を整えた。もちろんiPhoneシミュレータを起動したりリファレンス読んだりするのでXcodeは開きっぱなしなんだけど、それでも書くのをvimでやれるようになったら大分楽になった。

書くのは楽になったけど、そうなると今度は動作の検証もいちいちシミュレータ起動してあれこれ操作してってのが馬鹿馬鹿しくなってくる。そもそもテストも書かないでアプリ開発とか泣けてくる。幸いiPhoneSDKでもOCUnitが使えるので、ライブラリとして切り出せる部分を別プロジェクトに切り出した上でユニットテストを書くことにする。OCUnitがUnitTest Bundleっていうテンプレートを用意してくれてるので、プロジェクトにUnitTestのターゲットを追加してそれをビルドするだけでテストが走るようになっている。これならrakeからも簡単に実行できていい。

読めないよ

が。OCUnitでのテストをやったことある人は知ってると思うけど、OCUnitの実行結果はそんなに見易くない。

ocunitbuild

読めるかっつー話。もちろんこれはXcode上でも表示されるので、何の装飾もないのも当然。UnitTest Bundleのターゲットをビルドしたときに走るカスタムスクリプトを自分でごにょごにょすればいいっていえばいいんだけど、同じXcodeのプロジェクトを他の人がいじったりすることも有り得るのであんまりそっちで特殊なことをしたくない。Xcode上でビルドしたときはこれはこれでいいのだし。かといってそのままならビルドしてテストまで走らせてくれるようになってるのに、自前でビルドしてパッケージしてテストして表示するRakeタスクを作るのもなんだかなあ。それ両方メンテするの途中で面倒になって結局どっちか放置しそう。

見辛いなら見易くしてやればいいのです

というわけで、xcodebuildを実行した結果を綺麗に整形して表示するスクリプト書いた。さっきのテストがこんな感じに表示される。

rocu

こういうのでいいんだよ、こういうので。緑色になってるとテンション上がるだろ。

ちなみに、STAssert系、NSAssert系のアサーションや、実行時のエラーなどもちゃんと拾う(拾ってくれなきゃ意味ない)。failさせるとこんな感じになる。

rocu_fail

STAssertでこける、つまりテストケースで期待した結果が出てないときは赤字でアサーションのメッセージを出す。アプリ内のアサーションでひっかかってるときは黄色。それ以前にランタイムエラーで実行中に落ちる場合は赤の太字。ちなみに別に自前でテストツール作ったわけじゃないので(あくまでxcodebuild実行時の出力をフィルタしてるだけ)、ランタイムエラーがどこで出てるかまではわからない。そういうのはXcodeのデバッガとかでやってくだしあ。でもNSAssert系のマクロを使うとこけた場所も取れるので、ある程度厳密にやっておきたい値の検証とか積極的にNSAssert使うといいと思う。OCUnitの実行時にはNSLogでprint debugってわけにも行かないしねぇ。

一応コマンドにしてgistに上げてみた。色付き表示にするにはTerm::ANSIColorがいるので注意。色がいらなければ無くてもいい。使い方はソース読むといいと思います。元々Rakefileの中に書いてたのを抜き出してきてやっつけでコマンドにしたやつなので、全然動作の検証とかしてない。なんかツール郡にまとめようかと思ったけど満足しちゃったので。

表題通り日本Ruby会議2009に参加してました。一日目は参加できず、二日目の午後は途中から抜けなきゃいけなかったりでちょっと勿体なかったんですが、それでも本当に楽しかったのでつらつらと感想を。

まず何より先にスタッフに感謝です。今年もあの規模のイベントを回すのは本当に大変だったと思うんですが、どのスタッフの方もフレンドリーだし丁寧に対応してくれて、何よりみんな本当に楽しそうにしてたのが印象的でした。いいなー。本当にお疲れ様でした。

Reject懇親会

一日目は仕事で抜けられなかった&懇親会のチケットも買ってなかったので、Reject懇親会に参加。なんかみんな同年代な感じだったかな。RubyKaigiに集ってるのに「Scalaやろうぜ」「Objective-Cやろうぜ」とかそんな話したりしてたw

二日目以降も@repeatedlyや@kodukiたちと一緒に回ることが多かったけど、楽しかった!特に四国とか関西とかから来てる人達とは普段中々会うこともないので、貴重な機会に会えてよかったです。

Matz例外処理される

まつもとさんが遅刻して朝来なかった上、名札を忘れて受付で足止めくらうという事件に会場爆笑。Rubyに例外処理があって良かった、これで例外が捕捉されてなきゃRubyKaigi自体がセグフォで終了してたとこですね、わかります。

MacとRubyの話

三日目の午後は3会場のうち一つが、全部Mac絡みのセッションだったのでずっとそこにいたのだけど、予想以上に人多かった。RubyCocoa/MacRubyの注目度の高さに驚く。それと、iPhoneでRubyCocoaを使うセッションにいた人達のiPhone所有率が異常だった。会場のほとんどの人達が手を上げてるし。iPhoneのセッションだから当然ちゃ当然だけど。

MacRuby 0.5の話はとても面白かった。スピーカーのヴァンサンさんが言ってた通り、今0.5を使うのは「クレイジー」としか言いようがないんだけども、LLVMをバックエンドに使ってAOTコンパイラを持ってるとか、YARVをやめてVMを独自実装するとか、野心的な試みが多くて追ってると楽しい。まぁ、0.5俺の環境ではコンパイル通らなかったんだけどね…。

しかしこんだけ需要あるなら来年に向けてMacRubyネタでも用意しとこうかなぁ。実践MacRuby。

RejectKaigiに参加した

なんと勢いでRejectKaigiに参加してしまった。しかもネタはDT。去年は小さい方の会場で片付けが進む中2トラック同時進行、みたいなカオスな状態でやってた記憶があって、あれならなんとかいける、と思って参加したのだけど、今年はメイン会場でClosingの後そのままRejectに突入。うそん。すげーでかい会場なんですけど。なんかみんな全然帰らないんですけど。数百人いるよ、人。gkbr。

緊張のあまりうまくできた自信が無いけど、まぁ予定してたこと全部喋れたしそこそこ笑ってもらえたし、初舞台にしちゃ上出来でしょうか。うひー。みんなあれだぞ、あのプレゼンに対する賛辞の言葉は「これはひどい」だぞう。もっと罵声を浴せるべき。

ところで、終わったあとなんとyharaさんに声をかけられました。著者だ!著者の方が!せっかく面白い本なのにあんな形で使わせてもらってスイマセン!って気分でしたが、「あの本読んで自分で言語作ったって話、実はあんまり聞かないんですよねー」と。どうやら喜んでもらえてた様子。よかった…。「Rubyで作る奇妙なプログラミング言語 ~Esoteric Language~」、本当に面白いのでまだ読んでない人は買うと良いと思います。そんでオレオレ言語を作るといいと思います。俺でもできるんだから簡単です。

来年は

今年もとにかく楽しかったので、来年はもっとコミットできたらいいなと思った。それがスタッフなのか、またLTでもやるのかはわからないけど、お祭りは自分も中に飛び込んだ方がきっと楽しい。RubyKaigiだけじゃなくて、もっとRubyの中に踏み込んで行きたいなぁ、なんて思ったり思わなかったり思ったり。

トップレベル(モジュールやクラスの外)でメソッドを定義するとどうなるでしょうか??トップレベルでは、メソッドを定義するとデフォルトで private なメソッドになるので注意です。

01.#!/usr/bin/ruby
02.  
03.p self.class # Object
04.p self       # main
05.  
06.cmethod = self.class.private_methods
07.imethod = self.private_methods
08.  
09.def bar
10.  puts "bar method"
11.end
12.  
13.p self.class.private_methods - cmethod # ["bar"]
14.p self.private_methods       - imethod # ["bar"]

見ての通りですが、なぜかインスタンスメソッドとしてもクラスメソッドとしても利用できるようになっています。

これがちょっと気になったので調べてみました。どうもトップレベルで関数を定義すると Kernel モジュールに追加されるようです。

トップレベルでメソッドを定義したときの挙動が不思議 - (゚∀゚)o彡 sasata299's blog

またささたつさんが面白いことに気付いたっぽい。これ読んで俺も最初「えっ。なにそれこわい」と思ったんだけど、実はちょっと違うようです。ヒントはprivate_methodsの引数。

#!/usr/bin/env ruby

cm = Object.private_methods
mm = Kernel.private_methods
im = self.private_methods

def hoge
end

class Object
  private
  def piyo
  end
end

module Kernel
  def fuga
  end
  module_function :fuga
end

p Object.private_methods - cm         #=> [:hoge, :piyo, :fuga]
p Kernel.private_methods - mm         #=> [:hoge, :piyo]
p self.private_methods - im           #=> [:hoge, :piyo, :fuga]

p Object.private_methods(false) - cm  #=> []
p Kernel.private_methods(false) - mm  #=> []
p self.private_methods(false) - im    #=> [:hoge, :piyo]

はい消えた。private_methodsの引数は「スーパークラスから継承したメソッドを含めるか否か」で、デフォルトはtrue。falseにすると消えるってことは、mainオブジェクトにとっては「そのクラスで定義されたメソッド」だけど、ObjectクラスやKernelモジュールにとっては「どっかから継承してきたメソッド」だということ。これが本当に「クラスメソッドとインスタンスメソッド両方作られてる」とか「Kernelモジュールのモジュール関数になってる」とかだったら、この結果にはならないはず。

private_methodsの引数以外で、元記事のコードと上のコードで違う部分がある。トップレベルでメソッドを作るのとObjectクラスのクラス定義の中でprivateなメソッドを定義したのは同じである、という認識でいたんだけど、実際のところObjectクラスで作ってみたらどうなるんだろう、と思ってやってみたのが上のコードのpiyoメソッド。で、やっぱりhogeとpiyoは同じ振る舞いをしているように見える。でもそれはいいとして、やっぱり上のコードではトップレベルのメソッドだけじゃなくて、Objectクラスのクラスメソッドまでできてしまっているように見える。これはなんだろう。

"クラス"って何者?

ところで、さっきからクラスクラスと言ってるけど、Rubyの用語で気を付けなきゃいけないことがある。それはOOPの用語としての"クラス"と"Classクラス"は別者ってこと。俺がカタカナで"クラス"と書いてるときは、普通にOOP的な概念でのクラスのことを言ってるんだけど、上のコード中の"Object"はクラス定義そのものではなくて、実態は「"Classクラスのインスタンス"を作り、それを"Object"という定数にしたもの」だ。初めてのRubyを持ってる方は、8章のどこかにあるコラムを探してみて欲しい。小さい枠だけど、ちゃんと「クラス名とはClassクラスのインスタンスを作り、それを定数にしたもの」って話が書いてある。

で、Rubyのクラスのツリーを見て欲しい。"Object"はClassクラスの、"Kernel"はModuleクラスのインスタンスなんだけど、ClassクラスはModuleクラスの、ModuleクラスはObjectクラスのサブクラスになっている。つまり、上のコード中では"self"も"Object"も"Kernel"も、みんなObjectクラス(か、そのサブクラス)のインスタンスと言える。だから、Objectクラスにインスタンスメソッドを追加したつもり(実際その通りなんだけど)なのに、あたかもクラスメソッドが追加されたように見える。でも、"Object"だって"インスタンス"なんだもの、インスタンスメソッドが使えて何もおかしくないじゃない、という話、か。

ということで、最初のコードの謎が解ける。トップレベルのself、つまりmainオブジェクトは、Objectクラスそのもののインスタンスなので、private_methods(false)をするとちゃんとObjectクラスで定義したメソッドを出してくれる。で、"Object"や"Kernel"はObjectクラスのサブクラスのインスタンスなので、private_methods(false)するとObjectクラスのメソッドが消える。ということでした。

まとめ

  • トップレベル関数はObjectクラスのprivateなインスタンスメソッド(と、同じに見える、多分同じ)
  • クラス名やモジュール名の実態はClassクラスやModuleクラスのインスタンスであり、単なる定数
  • ClassクラスもModuleクラスも元を辿ればObjectクラス
  • Objectクラスのインスタンスメソッドはクラスメソッドやモジュール関数のように見える、なんてことだびっくりだ!

前回の続き。Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blogにあったうちrequireとincludeについて書いたので、次はmodule_functionとextendの話を。

module_functionは何のためにあるのか

まず前回のおさらい。

  • モジュールの中で特異メソッド(def self.hogeという形)として定義されたものはincludeするとクラスメソッドになるし、MyModule.hogeという形で呼べる
  • モジュールの中でメソッド(def fugaという形)として定義されたものはincludeするとインスタンスメソッドになる。が、MyModule.fugaという形では呼べない

で、module_function。これを使うとどうなるかというと、

module_function でインスタンスメソッドのように定義したメソッドを指定してあげると、そのメソッドは include しない場合は Zozom という名前空間で呼べて、include した後は include した package にメソッドが生えて、直接呼べるようになるようです。便利ですね〜。
Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blog

ということになる。そう、便利。なんで便利なのか。

  • (例えばMathみたいな)ユーティリティ的なモジュールは、いちいちモジュール名付けて呼び出すの億劫。includeして組み込み関数っぽく使いたい
  • でもトップレベルでincludeしちゃうと名前空間汚染しちゃうし、モジュールのほんの一部のメソッドしか使わないのに必ずincludeするのも変だし、何をしてるのか明示的にしたいときもあるし、そういうときははMath.cos(x)みたいにして使いたい
  • でもいちいち同じ定義のメソッドをとプライベートメソッド両方作るとかアホくさい

とまあそういうわけで、両方同時に作ってくれるというModuleクラスの便利メソッドです。それだけだった。例に上げたMathモジュールはほとんどのメソッドがmodule_functionされてるので、Math.cosでもinclude Mathしてからcosでも使えて便利。

extendは特定のオブジェクトだけに効力を発揮する

includeはModuleとmainオブジェクトのメソッドだったけど、extendはObjectのメソッド。加えて、Moduleのincludeはクラスメソッド(特異メソッド)だけど、Objectのextendはインスタンスメソッド。これは何をするものかというと、「特定のオブジェクトにだけ、クラスのincludeを呼び出したような効果を与える」もの。ちょっとわかりづらいけど、

include を extend にしてもまったく同じ結果(Zozomを付けなくても呼べる)でした。
Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blog

というのは微妙に間違い。というのも、トップレベルではまったく同じ結果なんだけど、以下のコードでは違う結果になる。

module Foo
  def hoge; "hoge!"; end
end 
  
module Bar
  def fuga; "fuga!"; end
end

class Baz 
end

include Foo
extend Bar

puts hoge # => "hoge!"
puts fuga # => "fuga!" 
    
baz = Baz.new
puts baz.hoge2 # => "hoge!"
puts baz.fuga2 # => NoMethodError

includeはクラスにモジュールの特徴を追加する、この場合Objectクラスを書き換えるので、Bazの中でもFooのメソッドはもちろん使える。でも、extendはオブジェクトの特異クラス、この場合は「"mainオブジェクトにとっての"Objectクラス」しか書き換えないので、大本のObjectクラスから派生したBazには影響がない。そして、こんなこともできる。

module Foo
  def hoge; "hoge!"; end
end 
  
module Bar
  def fuga; "fuga!"; end
end

class Baz
  include Foo
end

baz1 = Baz.new
baz2 = Baz.new
baz1.extend Bar
puts baz1.hoge2 # => "hoge!"
puts baz1.fuga2 # => "fuga!"
puts baz2.hoge2 # => "hoge!"
puts baz2.fuga2 # => NoMethodError

FooモジュールはBazクラスでincludeしてるのでbaz1でもbaz2でもhogeメソッドが使えるし、そのサブクラスにも引き継がれる。でもBarモジュールはbaz1でextendしたものなので、fugaメソッドが使えるようになるのはbaz1だけで、同じBazクラスのインスタンスのbaz2でも、もちろんサブクラスのインスタンスでも、fugaメソッドは未定義のままになる。というのがincludeとextendの違いでした。

おまけ: ModuleとClassの違い

まつもとゆきひろ コードの世界?スーパー・プログラマになる14の思考法まつもとゆきひろ コードの世界 スーパー・プログラマになる14の思考法
著者:まつもとゆきひろ
販売元:日経BP出版センター
発売日:2009-05-21
クチコミを見る

この本の最初の方にMix-inの概念について詳しく説明してるところがあるんだけど、それによるとモジュールってのはMix-inを言語としてサポートするために導入されたものらしい。モジュールとクラスの違いは実装の継承の仕方の違いに関わってくる。

  • モジュールは普通のクラスのサブクラスにはなれないが、include/extendの引数になって、クラスやモジュールに取り込める。また、複数取り込むことができる
  • クラスは他のクラスのサブクラスになることができるが、include/extendを使って他のクラスやモジュールに取り込むことはできない。また、継承元のクラスは一つに限られる

ということで、モジュールは「継承関係に縛られない、ある"役割"に関わる仕様や実装を定義する」のに使ったりする。Javaで言うところのインターフェースに近いけど、これは仕様だけをまとめるもの。Rubyのモジュールは仕様と実装をセットでまとめるもの。あとあれだ、関係ある変数・定数・ユーティリティーメソッドを一括りにする名前空間的な使い方もするね。というかMix-inに慣れないうちはそっちの方が良く見掛けると思う。

疲れた…

書いてる最中に「あれっ、これってこんなんなってたんだ」みたいなことに気付きまくって、どんどん記事書き直して調べなおす羽目になった。ひぃ。知らないで使ってるうちは大して違和感なかったんだけど、真面目に考え始めた直後はえらいややこしくて混乱した。大雑把にわかってきた後はなんてことないんだけどなー。

Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blogを読んでて、もしかしたらちょっと誤解があるのかなと思ったのでrequireとincludeとextendの話を。

requireはKernelモジュールのメソッド

Rubyで外部ライブラリを読み込むには、require を利用します。
Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blog

これはその通り。もっと具体的に言うと、requireは引数のファイル名のRubyファイルを読み込んで実行するメソッドです。引数が絶対パスだったときはそのファイルを、そうでない場合はロードパスを優先順位上位から辿って最初に見つかったファイルをロードする。で、拡張子まで含めて書いてある場合はそのファイルを、そうでない場合は.rbもしくは.soのものをロードするそうな。requireの他にloadというメソッドもあってこれはやることは同じだけど、requireの場合読み込むのは一度だけだけど、loadの場合は無条件に読み込む。なのでライブラリとかを読み込むときはrequireを使う。

includeはModuleクラスのメソッド

ただ、モジュールを読み込むときには include しないといけない、ということを「初めてのRuby」を呼んで知ったので早速試してみました。
Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blog

これは若干間違い。includeはモジュールを使うよって宣言じゃないので、

散々調べた結果、どうも include ってそのファイルの読み込みはしてくれないっぽいですね。あくまでもファイルの読み込みは require が担当っぽいです。
Rubyで自作の外部モジュールを読み込む方法 - include と extend と module_function - (゚∀゚)o彡 sasata299's blog

っていうのが正しい。

じゃあincludeって何なんだよってことなんだけど、これは「引数にモジュールを取り、includeしたクラスにincludeされたモジュールの性質を追加する」ということをしてくれる、というModuleクラスのメソッド。例えば、あるクラスBarがFooモジュールをincludeすると、スーパークラス(Barが何も継承してなければ暗黙のスーパークラスObject)との継承関係の間にFooモジュールを差し込んで、メソッドやクラス変数を探すときに「Barを見る→無かったらFooを見る→無かったらObjectを見る→無かったらKernelを見る→それでも無いなら無い」という風にしてくれる。つまりこういうこと。

class Bar
  include Foo
end
Bar.ancestors
# ancestorsはスーパークラスとインクルードしてるモジュールを優先順位順に返すメソッド
# => [Bar, Foo, Object, Kernel]

で、モジュールを定義したりincludeしたりすると、こういうことになる。

  • Hogeモジュールの中でself.hogeで定義されたメソッドは、Hoge.hogeとして使える
  • Hogeモジュールの中でfugaとして定義されたメソッドは、Hoge.fugaとしては使えない
  • includeすると、includeしたクラスのインスタンスからはfugaが使えるようになる

これは普通の継承関係にあるサブクラスとスーパークラスのことを考えると何のことはない。

  • def self.hogeはクラスにhogeというメソッドを定義する
  • def fugaはそのクラスのインスタンスにfugaというメソッドを定義する
  • クラスに定義されたhogeメソッドはKlass.hogeという形で呼べる
  • スーパークラスで定義されたfugaインスタンスメソッドはサブクラスのインスタンスからはfugaという形で呼べる

で、この「継承」の部分を「includeする」に読み換えるとあるクラスからモジュールをincludeしたときの挙動は分かると思う。同じように振る舞う。

トップレベルでのinclude

で。ここまでは「クラスがモジュールをincludeしたとき」の話。こっから先はちょっとややこしい。元記事ではトップレベルでモジュールをincludeしてるんだけど、これはちょっと面白いことになる。

トップレベルって何なんだって話なんだけど、トップレベル、つまりclass文やmodule文やブロックの中じゃない、一番外側で書いたRubyコードは、mainという暗黙のObjectクラスのインスタンスがselfである。なので、トップレベルで何らかのメソッドを呼び出すと、self.method、つまりmainオブジェクトのメソッドを呼び出す形になる。で、ここが微妙なんだけど、じゃあトップレベルでincludeとかやったらdef self.hogeで定義したやつがトップレベルでhogeになり、def fugaで定義したやつがmainオブジェクトのインスタンスのfugaになりそうなものだけど、そうはならない。大体mainオブジェクトはObjectクラスのインスタンスなのに、それのさらにインスタンスって何だよ意味わからんよ、って話ですよ。

実際はどうなってるかというと、def self.hogeで定義したメソッドは全てのクラスのクラスメソッドになり、def hogeで定義したメソッドは全てのインスタンスメソッドになる。なので、元記事のコードのhelloはめでたくObjectクラスのインスタンスであるところのmainオブジェクトのメソッドになりましたし、debugはZozom.debugとして呼べるのみならず別なクラスを作っても例えばYakitori.debugみたいに呼べる、という話になる。ややこしい。

mainオブジェクトからincludeする

これは何なんだ、一体どういうことだ…俺は、何かとんでもない謎に踏み込んでしまったのか…と言いたいところだけど、まぁ実は簡単な話だったりする。class文の中のincludeはModuleクラスのクラスメソッドであるところのincludeだけど、トップレベルのincludeは微妙に特殊で、トップレベルのincludeメソッドつまりmainオブジェクトのincludeメソッドを実行するとObjectクラスのincludeクラスメソッドを呼んだような挙動をする。そのために、次のようなことになる。

  • 全てのクラスはObjectクラスのサブクラスなので(1.9系は例外もあるけど、概ねそうだと思っていいので)、includeしたモジュールの中でdef self.hogeとして定義されていたメソッドがクラスメソッドとして使える
  • mainオブジェクトはObjectクラスのインスタンスなのでincludeしたモジュールの中でdef fugaとして定義されていたメソッドがインスタンスメソッドとして使える、そのためfugaが組み込み関数のように見えるようになった

長くなってきたので一旦まとめる

  • Rubyファイルを読み込むのはrequire
  • includeは「モジュールを使うよって宣言」ではなくて「モジュールが持ってる性質を、クラスに組み込むメソッド」
  • class/module文の中で使うと、モジュールの特徴をそのクラスに追加する
    • モジュールの中でdef self.hogeとして定義されてたやつはそのクラスのクラスメソッドになる
    • モジュールの中でdef fugaとして定義されてたやつはそのクラスのインスタンスメソッドになる
  • class/module文の外で使うと、モジュールの特徴をObjectクラスに追加する
    • モジュールの中でdef self.hogeとして定義されてたやつは全てのクラスのクラスメソッドになる
    • モジュールの中でdef fugaとして定義されてたやつは全てのクラスのインスタンスメソッドになる

調べてたらincludeが思いのほかややこしくて説明が長くなっちゃったので、module_funcfionとextendは次の記事に続く。

ちょっとだけ補足

def self.hogeという形式のメソッド定義は、主にクラスメソッドの定義として使われてるけど、正確に言うと「特異メソッド」というのを定義するもの。なのでselfの部分は実はどんなオブジェクトでもあまつさえ式でもいいし、クラスメソッドと呼んでるものの実態は「"クラスという種類のオブジェクト"の特異クラスの(インスタンス)メソッド」だったりする。はず。確か。特異クラスとか特異メソッドとかは…次の記事で書くかな。多分。

初めてのRuby初めてのRuby
著者:Yugui
販売元:オライリージャパン
発売日:2008-06-26
おすすめ度:4.5
クチコミを見る

sinatraとActiveRecordとERBでBBS作ったのでソースを公開してみる - だるろぐに触発されて俺もBBSを作ってみようと、ここ数日Sinatraをいじっていた。Sinatraさんは最近バージョンアップしてた気がするけど、どうやらちゃんとRuby1.9.1でも動くようだ。素敵。

で、順調に行くかと思ったんだけど、どうにも書き込みのspecが通らない。そこで初めて、POSTやGETでパラメータにマルチバイトの文字列が入ってると何かおかしいことに気付いた。

最初に書いたspecとアプリ側のコードを抜粋。

# coding: utf-8
require 'rubygems'
require 'rack/test'
require 'routes.rb' #sinatraアプリ

set :environment, :test

include Rack::Test::Methods

def app 
  Sinatra::Application
end

describe '投稿するとき' do
  it 'は投稿に成功したら一覧にリダイレクトすること' do
    @props = { 
      'user'  => 'セト',
      'body'  => 'べ、別にアンタの論文が心配なんじゃないんだから!',
    }
    Entry.should_receive(:create).with(@props)
    post '/add', @props
    last_response.status.should be_equal(302)
  end 
end
# coding: utf-8
require 'rubygems'
require 'sinatra'
require 'haml'
require 'sequel'

post '/add' do
  Entry.create(params)
  redirect '/'
end

DB = Sequel.connect('sqlite://db/trabbs.db')
class Entry < Sequel::Model; end

実際にはget '/'とかEntryのスキーマとかindex.hamlとかも用意してありますがそれは心の目で補完しておくれ。んで、期待してるのはこれでセトがツンデレな投稿をしてそれがDBに記録され、その後トップにリダイレクトされることなんだけど、これがコケる。

1)
Spec::Mocks::MockExpectationError in '投稿するとき は、投稿に成功したら一覧にリダイレクトすること'
 expected :create with ({"user"=>"セト", "body"=>"べ、別にアンタの論文が心配なんじゃないんだから!"}) but received it with ({"user"=>"\xE3\x82", "body"=>"\xE3\x81\xB9\xE3\x80\x81\xE5\x88\xA5\xE3\x81\xAB\xE3\x82\xA2\xE3\x83\xB3\xE3\x82\xBF\xE3\x81\xAE"})

ありゃ。UTF-8の文字列がくるべきところに、ASCII-8BIT、つまりバイト列がそのまま来ちゃってる。んむ、えーと、実際のバイト列を変更せずにエンコーディングだけ変えるのはforce_encodingだったよね。ということでforce_encoding('UTF-8')してみる。

1)
Spec::Mocks::MockExpectationError in '投稿するとき は、投稿に成功したら一覧にリダイレクトすること'
 expected :create with ({"user"=>"セト", "body"=>"べ、別にアンタの論文が心配なんじゃないんだから!"}) but received it with ({"user"=>"\xE3\x82", "body"=>"べ、別にアンタの"})

あ、あれ?なんか足りなくない?これはどういうこと?

Rack::Utils#escape

ところで、上では端折ったけど、実はspecを実行する際にこんなwarningが出ている。

/usr/local/lib/ruby/gems/1.9.1/gems/rack-1.0.0/lib/rack/utils.rb:13: warning: regexp match /.../n against to UTF-8 string

UTF-8に対してエンコーディングを無視して正規表現にマッチさせてる…うん、どうもこれが怪しい。さてこれはどこから出てるかというと、こんなコードから出ている。

# rack-1.0.0/lib/rack/utils.rb
module Rack
  # Rack::Utils contains a grab-bag of useful methods for writing web

  module Utils
    # Performs URI escaping so that you can construct proper
    # query strings faster.  Use this rather than the cgi.rb
    # version since it's faster.  (Stolen from Camping).
    def escape(s)
      s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
        '%'+$1.unpack('H2'*$1.size).join('%').upcase
      }.tr(' ', '+')
    end
    module_function :escape

    # Unescapes a URI escaped string. (Stolen from Camping).
    def unescape(s)
      s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
        [$1.delete('%')].pack('H*')
      }
    end
    module_function :unescape

    # 以下省略
  end
end

なるほどですねー。要するに、

  1. escape中でunpackするときに、String#sizeを使っている。1.8まではこのメソッドはバイト数を返してたけど、1.9では文字数を返すように仕様が変更されている。マルチバイトな文字列だと当然バイト数と文字数は一致しないので、途中で切れちゃうことになる。
  2. packして返ってくるのは単なるバイト列、つまりASCII-8BITのString。
  3. 1.8までは文字列がどんなエンコーディングであれ、unpackしてpackすればまた文字列に戻る。でも1.9ではunpackからpackの過程で元のエンコーディング情報は失なわれているので、明示的にエンコーディング情報を付加してやる必要がある

ということかしら。まず1に関してはString#sizeを使ってるところをString#bytesizeを使うように書き換えればいい。ちなみにRack::Utilsでは1.8でも1.9でも動くようにRack::Utils#bytesizeというメソッドが定義されているので、それを使うといい。というか、なんで使ってないんだろう。単に忘れてるんだろうなぁ…。

module Rack
  module Utils                                                                          
    def escape(s)
      s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/) {                                              
        '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase                               
      }.tr(' ', '+')
    end
  end
end

これをrackをrequireした後に書いてRack::Utils#escapeを上書きしてやると、ちゃんとescapeされるようになる。

次はASCII-8BITになっちゃってるのをforce_encodingしてエンコーディングを変える。これも本当はRack::Utils#unescapeを上書きして、その中でエンコーディングを推測してforce_encodingしておいて、アプリ側のフックでString#encodingを使って内部で使うエンコーディングに変換してやるのがいいかなぁとか思ったんだけど、それでいいのかなー。

んでもまぁ、今回はUAに応じてviewのエンコーディング変えるみたいなことはしてないし、UTF-8決め打ちで良いよね☆…というわけで、Sinatraのbeforeの中で、RUBY_VERSIONが1.9以上だったときはparamの文字列をforce_encoding('UTF-8')するようにしたった。良い子はちゃんとして下さい。

はい、これでちゃんとspec通りました。ふー。

…で。

そしたら、今度はRackをforkして直してpull request送ればいいんだよね。いいんだけど、やんなきゃなーとは思うんだけど、めんどいその辺のあんま自信ないので、誰か1.9でもちゃんと動くように直してRackの開発スタッフに教えてあげてもらえると助かります。助けてエロい人。

今日は勢い余ってサーバをUbuntu 9.04で再構築して、この際ついでだ、Rubyも1.9.1にしてやれ、aptで入れると色々と面倒だから自分でビルドしよう、ああそうだPerlの方も、CPANモジュールの管理とかアレな感じになるし、こっちもソースから入れ直そう、とかやってたら一日がかりのインストール祭りになってしまった。ええ、好きですよ、インストール。趣味ですよ。何か文句あるか!

とまぁ楽しくインスール祭りを終えて、いざアプリを動かそうと思ったら、「彼女が404」がまさかのエラー。あるぇーおかしいなぁ。ローカルでちゃんと1.9.1でも動いてたんだけどなぁ、と思いながらログを見たら「invalid byte sequence in US-ASCII」の文字が。うう?encodingはちゃんと指定してあるし、ローカルとサーバ上のファイルに特に違いはない。なんじゃろ、と追って行くとこのコードに辿り着く。

Haml::Engine.new(File.read(file)).render(Object.new, locals)

Fileから読み込んだのがUS-ASCIIのバイト列として扱われている。そのため、これに対して文字列操作しようとすると前述のエラーが出る。そういや、ここencoding指定し忘れてた。でも、じゃあ、なんでローカルだと動くんだっけ?

ああ、そうだ、Fileとか標準入力からの読み込むときは、指定が無ければ環境変数からencodingを決めるんだった。んで、ローカルはLANG=ja_JP.UTF-8、サーバはLANG=C。なのでサーバ側ではIOのencodingをUS-ASCIIだと判定してしまい、invalid byte sequenceになるわけだ。試しにexport LANG=ja_JP.UTF-8したらちゃんとSpec通った。と言うわけで、Fileから読み込むときにちゃんとUTF-8で読むように修正。

Haml::Engine.new(File.read(file, :encoding => 'UTF-8')).render(Object.new, locals)

おけー。これで動くようになりました。ちなみに、こんな風に書いても動く。

Haml::Engine.new(File.read(file, encoding:'UTF-8')).render(Object.new, locals)

Rubyの場合メソッドの引数の末尾がHashのときは{}を省略できるのだけど、1.9系なら{ :symbol => VALUE }みたいなのはさらに省略してキーワード付き引数みたいに表記できるんだった。ほほう。

「Ruby1.9対応とか楽勝だろ」みたいに思ってた時期が僕にも有りましたが、こんな感じでうっかりミスがポロポロでてきそうな予感。既存のライブラリを使うだけでも、かなりの頻度でIOや正規表現周りでinvalid byte sequenceとかinvalid encodingとか言われるので、いい加減1.9系でのトラブルシューティングに慣れといた方が良さげ。もちろんencoding絡みだけじゃないけどね、変わってるのは。それにしても1.9でも1.8.7でもばりばり働いてくれるRamazeさんは頼もしいな。

彼女のステータスを返す」のソースが読みたいというリクエストがあったので、少し書き直してgistに上げてみた。

gist: 112607 - GitHub

一応上の状態でrackupすれば動きます。hamlとrackの最新版が必要な他は特に何も要らないはず。ちなみに1.8.6、1.8.7、1.9.1では動くのを確認済み。んでPassengerで動かすときには、「PassengerでRackアプリを動かす」で書いたように、config.ruと同じ階層にpublicとtmpってディレクトリを作ってやって、コメントアウトしてあるRewindableInputWrapperを有効にしてやればいいはず。

ついでにRack::Testを使って書いたspecと、実際にGETしたりPOSTしたりしてどんなレスポンス返してるのか見るスクリプトも置いといたので、参考までに。

ぶっちゃけRackとHaml(とあとPassenger)で遊びたかっただけなので、「Sinatraのが楽じゃね?」「ってかそれApacheの設定だけでなんとかなりそうな…」とかいうツッコミは無しの方向で。ちなみにRackでオレオレWAFもどき作りにはそろそろ飽きてきたので、今はRamazeさんで遊んでたり。Ramazeさんいいな、気に入った。とはいえこのRackいじりは無駄ではなくて、こういうことやってからRamazeのソース読んでたら案外流れが掴めて良かった。

あとLast-Modifiedの日付とかもうほんと冗談なのでそういう小ネタに食いつくとか無しの方向で。食いつくなよ。絶対に食いつくなよ。絶対だかんな!

昨日のネタではRackで簡単なアプリを作ってそれを複数立ち上げたThinで動かしつつ、表のApacheからmod_proxy_balancerで適当にプロキシしてやるって構成にした。Railsとかでもよくやるので慣れてるし、扱い易いので好きな構成だ。

ただ、今回の遊びでちょっとやってみたかったけことがある。何かというと、Passengerの導入。mod_railsとかmod_rackとか呼ばれてるアレ。スタンドアロンのサーバではなくてApacheやnginxに組み込んで使うタイプで、パフォーマンスもそれなりに良いし使い易いという話を聞いてたので気になってはいた。でもRails使わなくなってからなかなか試してみる機会がなかったので、この際ついでだ、とやってみることにした。

設定は簡単

インストールについてはPassengerのページでも見てもらうとします。別に何のことはない、gemからインストールできるし、俺が借りてるサーバ(OSはUbuntu 8.04)ではapt-getでさくっと入った。

んで。例えば、次のような構成のRackアプリができてたとする。もちろんこの状態でrackupすれば普通に動くのが前提。

/var/www/rackapp
         |
         +-- config.ru
         |
         +-- lib/
         |
         +-- view/

で、rackapp.example.orgってのでアクセスすると上のアプリに処理が移るようにしたいとする。まず、/var/www/rackapp以下にpublic、tmpってディレクトリを作る。publicはhtmlとかcssとかの静的ファイルを置く場所。tmpには、あとで説明するけど、restart.txtってのを置いておく。こうなる。

/var/www/rackapp
         |
         +-- config.ru
         |
         +-- lib/
         |
         +-- view/
         |
         +-- public/
         |
         +-- tmp/
              |
              +-- restart.txt

そこまで用意できたら次はApacheの設定。

<VirtualHost *:80>
ServerName rackapp.example.org
DocumentRoot /var/www/rackapp/public
RackBaseURI /
</VirtualHost>

これだけ。実にさっぱり。特に指定をしなければこれでRACK_ENV=productionでアプリが起動する。色々細かい指定はできるけど、まぁそれは必要になったときにいじればいいよねとりあえず。あと、アプリの修正をした場合はいちいちApacheを再起動しなくても、

$ touch tmp/restart.txt

とかやってrestart.txtのタイムスタンプを更新してやるとPassengerがアプリを再読み込みしてくれる。うわーいできた。なんだよマジ簡単じゃんよー、とか思ってたら甘かった。

な、何も出ないぞ…。

早速これでブラウザからアクセスしてみたら、全く何も表示されない。なんだこれ。config.ruでRack::Lintをuseしてるんだけど、ログ見たらそいつが何か警告出して処理を止めてる。うーん、だけど、全く見に覚えのない警告なので、困惑しつつRack::Lintを外してみる。案の定エラー出て落ちる。どうやら見てると、アプリのcallメソッドに渡されたenvからRack::Requestを作るときにこける様子。なになに、env['rack.input']にrewindメソッドが無い…?

色々調べてたら、Rack::RequestはrewindできるIOオブジェクトが必要なんだけど、PassengerはrewindできないIOを渡してくるらしい。Rackの開発グループでも議論になってる様子。これRackベースのWAFとか作ってると結構致命的だよなー。

rewindが無いと駄目なら、rewindできるオブジェクトにしちゃえ

とまぁつまりそういう話なわけですよ。Passengerが渡してくるIOオブジェクトがrewindできないなら、rewindできるIOオブジェクトに変換するなりラップするなりしてしまえばいいや、と。やっぱり同じこと考えてる人もいるみたいだし、そもそもその人が何を参考にしてるかというとRailsのActionControllerの実装だったりする。というわけで、それにならってとりあえずこんなミドルウェアを作ってみた。

# netakit/rewindable_input.rb
module NetaKit
  class RewindableInput
    def initialize(app)
      @app = app
    end
    def call(env)
      unless env['rack.input'].respond_to?(:rewind)
        env['rack.input'] = StringIO.new(env['rack.input'].read)
      end
      @app.call(env)
    end
  end
end
# config.ru
require 'netakit'
require 'netakit/rewindable_input'

use NetaKit::RewindableInput
use Rack::CommonLogger

map '/resource/kanojo' do
  run NetaKit::Resource::Kanojo.new
end

NetaKit、はfaultier.jpで動いてるアプリのnamespaceなので特に気にしない方向で。まぁこんな感じにしてやると、rack.inputがrewindできないIOだったときはStringIOに変換してからアプリに渡してくれるようになる。やってみたらこれでちゃんと動くようになった。まぁこのままだと入力を使う使わないに拘わらず(例えばRack::Responseを生成せずにenvを生で扱い、かつinputを読む必要のない処理だけをやるようなミドルウェアとかを通してるときにでも)毎回inputをreadしちゃってアレげなので、ちゃんとやろうと思ったら何かのオブジェクトでラップしてやって呼ばれたときに変換かけるようにする方がいいかも。例えばさっきのCloudKitだとこんな風に実装してるとか。まぁ、上に書いたのでも動くっちゃ動くのでお試し程度なら十分だけど。

お手軽感は確かに

というわけで意外に手間どったPassenger対応だったんだけど、rack.inputの問題を除けば簡単に設定できて中々良い。立ち上げるプロセス数の調整とかもApacheが勝手にやってくれるわけだし、静的ファイルはアプリ通さないで返すようにするのにも特に設定が要らないのも楽だ。動かすのがApacheだけなのも余計なこと考えないで済むしいいな。

そういえば、一応参考程度にApache BenchでPassengerとThin+mod_proxy_balancerとのパフォーマンス比べてみたんだけど、ぶっちゃけ殆ど差はない。と言っても/resource/kanojoにひたすらGETリクエスト送っても殆ど静的なレスポンスを返すだけなので本当に参考程度。同時アクセス数を増やしてみたら若干数値が違ってたけど、Thinの立ち上げてるプロセス数次第で変わってくるだろうし、どうせ実運用になるとPassengerの場合でももう一段フロント立ててごにょごにょやるだろうからなぁ。RailsとかMerbとかでがちっとアプリ組んで、mongrel、lighttpd、WEBrick、fcgiとか色々試してみないと何とも言い難い。個人で作ったものくらいだったらPassengerのがやること少なくてお手軽かも、くらいには思った。あとまぁレンタルサーバだったら各自アプリケーションサーバ起動させるとか許さないだろうけど、Apacheに組み込めるんなら入れといてくれるところとかありそう。まぁ正直どっちでもいいな。とりあえずしばらくPassengerで運用してみよう。

あんま関係ないけど

rack.input問題調べてる最中に偶然辿り付いたCloudKitがちょっと気になる。面白そうだし、atomserver作るのに参考になりそう。あとでいじってみよう。

↑このページのトップヘ