Railsで本当にやった○○な話
そのいち。テストを書き加えるファイルを間違えたのに気付かず悩む。関係ないコントローラをテストしていたよ。
そのに。distinctを付け忘れる。やはり悩む。
AWDwR3刊行記念トークセッション
昨日(2009-12-03)、池袋のジュンク堂本店であったRails本の第三版[rakuten]出版記念トークセッションとその後の懇親会に参加した。懇親会に出たはいいが、油断していたら帰れなくなりそうな時刻になってしまっていて、ドタバタと逃げるように会場を後にした次第…… われながらひどい。
トークセッションではRSpecとTest::Unitの比較の中での大場さん言葉が印象に残った。assertを書こうとするとき、一度考えを止めて順番をひっくり返しているのだということに、RSpecを使うようになって気付いたそうだ。
私はRSpecについてわかってないので比較としてはわからないけど、assertを書くときのつまづくような感覚はなんとなくわかる。それが大場さんのいうソレと同じなのかは、これまたわからないのだけども。でも、なるほどそういうものかということで、RSpec関心がかなりわいてきた。
あと次の(今月出る)WEB+DBに、松田さんによるRails3記事があるということだった。タイムリーでよいなあということで期待。
懇親会では、えにしテック島田さん、万葉jugyoさん、でじたまコンピューター朝倉さんと、なんだろう、仕事の話とかいろいろを聞いた。途中からigaigaさんにも話を聞けた。大変興味深い。オーム社の森田さん、鹿野さんからもいろいろ話を聞けた。ネコのいる統計本を読まなくては。
RailsによるアジャイルWebアプリケーション開発 第3版 2
レビューに少し協力した関係で
RailsによるアジャイルWebアプリケーション開発 第3版[rakuten]を一足早く入手した。レビュー中には24〜27章に手をつけられなかった。せっかくいただいた本なのでまずはそのあたりから読んでいこうと思っている。
例によってぶ厚くて、しかも辞書のように(ちょっと大げさ)薄い紙の本。このボリュームを改めて通読しようとすると、まず、始めるぞという意識的に気持ちを高めて取り掛かることになる。というのは、いくらかでもRailsを触ってきたり、コードを読んだり、コードを書いたり、という経験があるからだ。
けれども、実際に通読してみると(まあ途中までなんだが)、思った以上に広い範囲のことが書かれているのがわかる。
中級者以上を対象としたRails本がたくさん出てきているなかでは、入門者も対象とするこの本は、もしかするとちょっと距離をとられてしまいがちなのかもしれない。でももし自分の知識にどこかに不安があったり、あるいは安定感に欠けるなどと思うことがあったりするなら、改めてこの本にあたってみてはどうだろうか。間口が広いわりには「奥が深い」ところのあるRailsなので、何かしら発見があるかもしれない。
もちろんRailsをこれから始めるという人にもおすすめできる。
上述の通りかなりのボリューム感だが、本を読み始めてほどなく、ほんのちょっと手を動かすだけで、実際に動くRailsアプリケーションを作ることができる。ボリューム感はむしろどこまででも付き合ってくれる安心感につながる。Railsの入門書もやはりたくさん出てきている。好みや相性もあるからどんな人にでもとは言えないが、たくさんある本の中から選びかねているならまずはこれ、とは言えそうだ。
なお、対応しているRailsのバージョンは次の通り。
本書はRails 2.2に基づいています。具体的には、本書のコードはRails 2.2.2のRubyGemsパッケージにより動作確認を行っています。
(5ページ、原書についての記述)Rails 2.3の変更点については、注で補足しています。また、Rails 2.2.2およびRails 2.3.3以前のバージョンには脆弱性がありますので、本番環境で利用する場合は、2.2.3以降(2.3系の場合は2.3.4以降)のバージョンにアップグレードしてください。
(iiiページ、訳注)
Passengerで本当にあった○○な話
Passengerを使っていて実際にやってしまったダメーな思い出話。(けっこう前のことなので今の状況とは少し違うかも。)
間違ったキャッシュが返る
この日記にはtDiaryからTypoに移行したという経緯がある。移行のためのあれやこれの手間はかかったものの、一通りの作業を終えるとおとなしく動作していた。
ところが、ある時、ブラウザからのアクセスにatomデータを返していることに気付いた。動作を追ってみると、どうも何かの加減で間違ったキャッシュファイルがレスポンスに使われているように見える。そこで、とりあえずの回避策として、生成されていたatomキャッシュを削除した。するときちんとhtmlデータが返されるようになり、新たにhtmlのキャッシュファイルが生成されていることを確認できた。
気になって調べてみると、atomキャッシュだけが生成されているページが他にもいくつかある。先にatomへのアクセスがあれば当然このようになるわけなので、それ自体は問題ないはず。だが、これがあるとhtmlで返すべきところが返らなくなってしまうようだ。
ある期間この現象は困った謎のままであった。まったく見られなくなるわけでもなく、ともかくは動いているということもあって、場当たり的な対処を繰り返して過ごしてきた。
ここでTypoが作るキャッシュを考えてみる。Typoは生成するレスポンスによってhtml、atom、rssなどのキャッシュをファイルとしてpublic以下に作る。これらのファイルは、Passengerの通常通りの動作としてTypoが関与することなくレスポンスに使われる。したがって、問題のhtmlリクエストに対してatomデータを返している容疑者はTypoではなくPassengerかApache HTTPサーバである。ということに、かなり時間をかけた上で気付いた。
もう少し正確に言うと、ようやくそこに目を向けることができた、という感じかもしれない。となれば、まっ先に疑うべきであった点が一つ。MultiViewsだ。
ところで、前述の通り、この日記はtDiaryからTypoに移行した。tDiaryのときの設定は以下のようなものだった。
Alias /diary /path/to/diary <directory /path/to/diary> # tDiary Options -MultiViews </directory>
これをTypo+Passengerにかえた際に、/path/to/document_rootから/path/to/diary/publicへのシンボリックリンクを作成した上で、次のように変更した。(実際にはtDiary→Typo+FCGI→Typo+Passengerと設定を変えてきているため、その跡が少し残っている。)
#Alias /diary /path/to/diary/public <directory /path/to/diary/public> # Typo Options -MultiViews </directory>
気持ちの上ではMultiViewsはオフのままにしたつもり。だが、もちろん、そんなことはなく、/path/to/document_rootで有効になっているMultiViewsは/path/to/document_root/diaryでも有効になる。そしてTypoが作り出すURLには拡張子がついていない。ブラウザからのアクセスは/diary/fooに対するものとなる。この時htmlキャッシュがなくて、atomキャッシュやrssキャッシュがあれば。そう、MultiViewsの機能により(もちろん設定にもよるが)htmlではなくそれらのデータがレスポンスとして送信されてしまうのであった。
Passengerじゃない!
FastCGIで動作させていたあるRailsアプリケーションをPassengerで動かすことにした。ところが、なかなか作業がはかどらない。移行をしているせいもあって、いろいろやってみたあげく、変わらずFastCGIで動いてしまうというのを何度か繰り返していた。
と、ここで、FastCGIを動かなくしてしまえばよいではないかと思い付いた。そもそもPassengerに移行した後はFastCGIを使わないのであるから、これはどちらにしても良いことである。
さっそくそのようにしてみたところFastCGIで動作することはなくなった。
よかった、よかった。別の作業に取りかかろう。……あれ? なんだか重い? アプリケーションは動いてはいる。エラーも出ていないし、返ってくるデータにも問題はない。だけど、あれ? レスポンスがものすごく悪い?
FastCGIのプロセスがなくなったことで安心してしまったが、そういえばPassengerのプロセスは動いていただろうか。
あわてて見直してみると、Passenger配下にあるはずのインスタンスがない。では、どうやって動いているのだと追ってみれば、なんとも困ったことにアプリケーションはCGIで動いていたのだった。
入門書の次に読むRailsデプロイ
とあるきっかけから、監訳者の一人の橋本さんを通してオライリーさんから
Railsデプロイ[rakuten]をいただいた。Railsプログラミングがひと通りできるようになった、常用しているマシンの上で動作させてきた、自分以外の人々に向けたサービスをこれから始める。本書はそんな人々を助けてくれるだろう。
プライベートに運用するアプリケーションを除くと、普段は自分がログインすることがほとんどないようなマシンがアプリケーションを動かすための場になる。そのようなマシンは一つだけではなく、用途ごとに複数のマシンを並べて動作させることが多い。このようなマシン環境下でアプリケーション群を正常動作させ続けるには、いつも使っているマシンでアプリケーションを運用するのとは異なった視点・手法・作業が求められる。
たとえば、アプリケーションを動かすのに必要なハードウェア環境…… はともかくとしても、ソフトウェア環境を整えなければならない。それはRubyのインストールから始まるかもしれないし、もしかするとRubyをインストールするための環境作りから始まるかもしれない。また、HTTPサーバを設定しなければならないだろうし、複数あるマシンがうまく連携できるようにもするだろう。アプリケーションのリリースは複数のマシンが対象となる。マシンの用途が違えば作業も違う。そして、マシンを増やすことがあればそのたびにそれらすべてを行う必要がある。いきなりなにもかもやろうとすると大変だ。
本書を読むと、手元で動かす→他のある程度整備されたホストで動かしす→VPSを借りて動かす→専用ハードを借りて……、といった具合にじょじょに規模を拡大していく工程をひと通りながめることができる。その内容は単に「Railsアプリケーションをリリースする」というだけではなく、複数のマシンでサービスを運用するための機能分散の方法や負荷分散をするためのリバースプロキシの設定など広い範囲をカバーしている。
そのうえ、複数のデータベースを運用するためのMySQLの設定とアプリケーション側での対処、チューニングのためのベンチマークやプロファイルの取り方、キャッシュの使い方とキャッシュ乱用への注意など、アプリケーションをサービスとして運用するのに必要な領域についても実践的に解説する。そのような点から、アプリケーションを作れるようになり、さあこれからデプロイしようといったストーリーの入口で本書は活躍するだろう。
少々残念なのは、今この時期にPassengerが扱われていない点。もちろん出版時期などからいたしかたないところではあるのだが、カバーしている範囲の広さからすると実におしい。また、Railsからはやや離れたところでの説明や表現にはいくつかひっかかる点もあった。一つあげると125ページの「Linux上でRailsアプリケーションを動作させるためには……最低限でもCやC++のコンパイラ……が必要」といった記述で、これはRMagickなどをRubyGemsで運用することなどを前提にしているのであるが、できるならコンパイラは避けたいところと考える人も少なくないはずだ。(他のものも含めて出版社の方に伝えておいた。)
一方、本筋とやや離れたところでちょっと感心したのは、Apache HTTPサーバの設定作業の中で、a2enmodやa2dismodなどに触れている点だった(本書ではUbuntuを主な環境としている)。この種の特定のOS環境(この場合はDebian/Ubuntu)に独特な手順というのはともすれば流されてしまいがちである。もちろん明確な方針があって別のやり方で運用するのは構わない。だが、マシンはいつか自分の手を離れるものであるとすると、理由がなければできるだけその環境の流儀に従っておいたほうがよいと思う。考えてみればRailsだってそうなのだから、環境整備でも同じようにしたってよいだろう。
追記(2009-07-24)
読んでいてひっかかった点は編集さんに送ったが、そのうち正誤表に載っていないものや載らなそうなものを以下に挙げておく。
- 56ページ15行目「コマンドラインに関する知識と何でもGoogleで検索してみる」
- 「知識を何でも」?
- 67ページ下から10行目「インストール中にどのような種類の設定を行うか聞かれたら"Internet site: ... "(メールはSMTPを使って直接送受信される)という選択肢を選んでください」
- 他のところでメール関連のことを扱っているのかなとも思ったのだけど特別に前提条件はなさそうなので、本文の記述に従うと外部からアクセス可能なSMTPサーバをたてるということになりそう
- となると、一般的な選択という点ではローカルホストのみで動作させるくらいにして、必要に応じてそのような設定をしよう、というような書き方のほうがよいのでは
- 106ページ囲み
- ファイルを/etc/init.dにコピーするだけでよいように読めるが一般にそのようなことはないはず
- Debian系であればupdate-rc.d、Red Hat Linux系であればchkconfigでの操作が必要になる
- 107ページ囲み「flexとbisonそしてbyaccをインストールしましょう」
- flex、bison、byaccのうちのいずれか、ではないのかな
- 110ページ15行目「Linuxシステムでは、initは/etc/init.dに置かれるすべてのスクリプトの実行を担当しています」
- initはすべてのプロセスの祖先ですから間違いとはいえないが、 ここでいおうとしているinitの用法と /etc/init.d(より正しくは/etc/rc*.d)以下のファイルの運用はまた別だといっても差し支えないと思いう
- /etc/init.d(略)以下のファイルにより起動したサービスは /etc/inittabで管理されるプロセスとは違ってinitにより再起動されることは通常ない
- 110ページ訳注「sudo update-rc.d monit remove」
- /etc/init.d/monitが残っている場合、update-rc.d -f monit removeとする必要がある
- そうでなければrm /etc/init.d/monitした上でupdate-rc.d monit removeとする
- 110ページ下から4行目「Monitを使ってMongrelを管理するようになったら…mongrel_cluster/recipesを使う必要はなくなりました」
- mongrel_cluster/recipesが唐突に出てきている…… ような気がする
- 読み落としてしまっただけかも(検索可能な何かがあればなあ)
- 111ページ下から4行目「FastCGIのゾンビプロセス」
- 「なぜか居残り続けてしまう、仕事をしないfastcgi管理下のプロセス」にすぎず、UNIX一般にはこのようなプロセスをゾンビとは呼ばないように思う
- 実際、reaperはfastcgiプロセスに対してシグナルを送っているわけだけど、本来の意味でのゾンビプロセスはそもそもシグナルを受け取れる状態にない
- 参考: プロセス - 終了状態
- 117ページ15行目「ハートビート」
- 外部から動いていることを確認すること自体をハートビートとはあまり呼ばないような気が
- 一般的には生存証明を自らするようなものをそう呼んだり、あるいは、HAクラスタなどでサービスが動作していることを待機系とで情報交換するような仕組み全体を指すことはあると思う
- いずれにしても本文で説明されているのはNagiosなどがするような監視であって、死活監視などと呼ぶほうが一般的ではないだろうか
- 127ページ下から二行目「CNAMEレコードはAレコードのエイリアス(別名)のようなものです。対象のAレコードは自分のものでも他人のものでもかまいません」
- DNSの「リソースレコード」と「名前」が混同されているように思える
- CNAMEレコードでは名前に対応する正しい名前を定義する
- CNAMEレコード全体をもってエイリアスと表現するのは間違いとまではいえないが、CNAMEレコードの対象がAレコードというのは表現としておかしいのではないか
- 実際、CNAMEレコードを持つ「名前」に他のリソースレコードを付けてはならないことになっているはずで
- 129ページ10行目「大きな値(例えば7日)を指定すると、ブラウザなどのクライアントソフトウェアがDNSに問い合わせを行う回数を削減でき」
- 「クライアントソフトウェアがDNSに問い合わせを行う」というのはリゾルバの動作で、リゾルバの動作にはTTLはリゾルバの動作に影響を与えない…… のではないかしら
- MS-Windowsなど、ある種の環境ではDNS問い合わせについてのキャッシュ機構があるようだが、それらを考えにいれたとしても実装依存というのがやっとだと思う(MS-Windowsの機構はキャッシュサーバの一種なのではないかしら?)
Railsキャッシュ、FastCGIとMultiViews
Typoにしてしばらく気付いていなかったのだが、次のようなアクセスパターンで404 not foundが起きる。
- キャッシュをクリアする
- 「/typo/2008/10」にアクセスする
- 「/typo/2008/10/11/ほげほげ」にアクセスする
- 404「The requested URL /typo/2008/10.html/11/ほげほげ was not found on this server.」となる
何が起きているかはmod_rewriteのログを見ればすぐに分かる。
[example.jp/sid#8f135d0][rid#8f391e0/subreq] (3) [perdir /typo/public/] add path info postfix: /typo/public/2008/11.html -> /typo/public/2008/10.html/11/ほげほげ [example.jp/sid#8f135d0][rid#8f391e0/subreq] (3) [perdir /typo/public/] strip per-dir prefix: /typo/public/2008/10.html/11/ほげほげ -> 2008/10.html/11/ほげほげ [example.jp/sid#8f135d0][rid#8f391e0/subreq] (3) [perdir /typo/public/] applying pattern '^$' to uri '2008/10.html/11/ほげほげ'
問題はなぜこうなるか。FastCGIなんてどこででも使われているのだから、手元の環境の何かが間違っているのだろう。そうは思いつつも、このことに気付いたのが今朝出掛ける少し前だったので、さっくり動けそうなPassengerに逃げてしまった。
そして今もう一度、別の環境で再現させようとしたのだが、どうにも再現できない。どうしたものか、というところでMultiViewsの動作を思い出した。
問題は/typo/public/2008/10.htmlがあることにより、/typo/public/2008/10/へのアクセスがこのファイルに向けられてしまうところにある。拡張子が補完されてしまっているわけで、この動作がまさにMultiViewsのそれだということにようやく気付いた。そこで再現できなかった別環境でMultiViewsを有効にしたところ、現象が再現するのを確認できた。
というわけで、Typoのように階層的でかつ中間ディレクトリ名と同じベース名のキャッシュファイルが作られるようなときにはMultiViewsとの相性が問題となることがある。もっとも、そのようなところにRailsアプリケーションを配置していたのがそもそもの原因とも言えるが。
ともあれ、運用はFastCGIでもPassengerでもどちらでもいいので、しばらくこのまま様子を見てみようと思っている。Passengerも動かしてみようとは思っていたところでもあるし(とか、いいつつ、typoからまた戻ったり、また別のものにしたりするかもしれないが)。
Typoインストールメモ
まずTypoのコードを入手する。いろいろなも味に依存しているのでRubyGemsを使うのが楽。
$ GEM_HOME=/tmp/GEM gem install typo
Typoではサイトをセットアップするときに必要なものをすべてコピーするので、インストールしたgem群への依存関係は一応なくなる。ただし、typoコマンドを通じてバックアップなどの管理操作ができる。そしてそのような操作のためにはインストールされたgemが必要となる)。
$ sudo -u www-data \
env GEM_HOME=/tmp/GEM \
/tmp/GEM/bin/typo install path/to/install/dir \
db_user=dbuser \
db_password=dbpass \
db_name=dbname \
web-server=external # FastCGIの場合
これでTypoが動作する環境ができるが、.htaccessは作ってくれないのでこれを作っておく(FastCGIで動かそうと思うので)。また、Rails 2.x系ではw3mなどでアクセスすると406エラーになるので回避コードを入れておく。
$ rails /tmp/t $ sudo -u www-data \ cp /tmp/t/public/.htaccess path/to/install/dir/public $ rm -rf /tmp/t $ sudo -u www-data \ vi path/to/install/dir/putlic/.htaccess # 調整 $ cat <path/to/install/dir/config/initializers/w3m.rb
Mime::HTML.instance_eval { @synonyms << "text/*" }
Mime::LOOKUP["text/*"] = Mime::HTML
E
今回、tDiaryから移行したデータがあるので、一部の表現のために書いた互換プラグインを置いておく。
$ sudo -u www-data \
cp -a typo_textfilter_{asin,tdiarycompat} \
path/to/install/dir/vendor/plugin
最後にApache HTTPサーバ側のその他の調整をしてリロードする。
$ sudo vi /etc/apache2/sites-available/site # その他調整 $ sudo /etc/init.d/apache2 reload
ブラウザでアクセスすると最初のユーザ登録ができる。tDiaryからのデータ移行の都合のためspamまわりの設定を残して、その他の設定をしておく。その後でデータを流し込む。
$ sudo -u www-data \ path/to/install/dir/script/runner td2typo.rb tdiary.dump
終了後、残しておいたspamまわりの設定をし、動作確認をする。
この環境ではFastCGIをmod_fcgidで運用しているのだけど、外部リソースにアクセスしまくるページなどでIPCCommTimeoutにひっかかることがあるようだった。tDiaryをFastCGIで動かそうとしたときにはIPCConnectTimeoutを大きめにしなければならなかったのだけど、ここではCommのほうを大きめに設定した。
実はnet/httpのタイムアウトのところでエラーになっていたのを勘違いしてしって、ずいぶん遠まわりをしてからこのことに気付いた。いかんいかん。
Rails 2.0.2とRuby 1.8.7のString#respond_to?
ところが(なのかな?)Rails 2.0.2にも同じバグがあって、これも顕在化した。よって、次のようになるのは1.8.7を1.8.7-p17にしても変わりない。
$ ./script/console
Loading development environment (Rails 2.0.2)
>> "" + ActiveSupport::Multibyte::Chars.new("")
ArgumentError: wrong number of arguments (2 for 1)
from (irb):1:in `respond_to?'
from (irb):1:in `+'
from (irb):1
>>
つまりこれはActiveSupport::Multibyte::Charsで定義されているrespond_to?が引数を一つしか取らないからで、これを二つ取るようにすればよい。というわけで、config/initializers/fix_multibyte_chars_respond_to.rbというようなファイルを以下の内容で作ることで回避できるような気がする。
if defined?(ActiveSupport) &&
defined?(ActiveSupport::Multibyte) &&
defined?(ActiveSupport::Multibyte::Chars)
mc = ActiveSupport::Multibyte::Chars.new("")
begin
"" + mc
rescue ArgumentError
raise unless mc.method(:respond_to?).arity == 1
class ActiveSupport::Multibyte::Chars
def respond_to?(method, ip = false)
super || @string.respond_to?(method, ip) || handler.respond_to?(method, ip) ||
(method.to_s =~ /(.*)!/ && handler.respond_to?($1, ip)) || false
end
end
end
end
Rails 2.0.2とRuby 1.8.7のString#chars
Ruby 1.8.7からはString#charsが定義されるようになった。これがRails 2.0.2なんかではまずくって、たとえば"hello".firstでNoMethodError: undefined method `[]' for #<Enumerable::Enumerator:0xb6f3f5b4>などとなる。これはRailsのように積極的に組み込みクラスを改変しているものの定めといえる。
Rails 2.1.0を見てみると以下のようになっていて、Ruby 1.8.7のString#charsを削除し、Rails(ActiveSupport)が定義しているcharsを使おうとしている。
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module String #:nodoc:
unless '1.9'.respond_to?(:force_encoding)
# Define methods for handling unicode data.
module Unicode
def self.append_features(base)
if '1.8.7'.respond_to?(:chars)
base.class_eval { remove_method :chars }
end
super
end
[...]
Rails 2.0.2でも同じようにするには、Rails側に手を入れたほうがてっとり早いとは思うのだが、ひとまずそれはしないことにするならばconfig/initializers/remove_string_chars.rbを以下の内容で作っておくというやり方はどうだろう。
unless '1.9'.respond_to?(:force_encoding)
String.class_eval do
begin
remove_method :chars
rescue NameError
# OK
end
end
end
script/consoleやscript/server(アプリケーションはTracks-1.6)で簡単な動作確認をしてみたところ、ひとまず前述の例外を回避できているようだ(Ruby 1.8.7、1.8.7-p17および1.8.6で確認した)。ただしそれ以外の環境やアプリケーションでどうなるかは確認していない。


