Thursday, October 30, 2008

How to skip ActionController::Filters with block

before_filter :foo
def foo
...
end

メソッド名のシンボルを与えてフィルターを指定する場合、
skip_before_filter :foo

でスキップできるけど、
before_filter do |controller|
...
end

ブロックを使った場合スキップする方法がわからないので ActionController::Filters の実装を調べたら、
before_filter :identifier => :foo do |controller|
...
end

とすることで、
skip_before_filter :foo

できることがわかった。

:identifier オプションは ActiveSupport::Callbacks::Callback のアトリビュートなので、 ActionController::Filters の実装が ActiveSupport::Callbacks::Callback を使っていない 2.0.X 以前のバージョンでは使えないと思われるので注意。

Monday, October 27, 2008

Fixing false latin1 encoding MySQL database into utf8

Rails 2.2 には mysql pure ruby adapter が含まれていないので、アップデートしたときに mysql native adapter を入れていない事に気がついた。
 sudo gem install mysql

その後、アプリケーションを再起動したら、データベースに保存されている文字列の表示が思いっきり文字化けしてしまった。
database.yml をよく見てみると、
 encoding: utf-8

しまった。 MySQL adapter の場合、
 encoding: utf8

と書かなければいけなかったのだ。
MySQL native adapter のデフォルトエンコーディングは latin1 らしいので、一度、 latin1 としてダンプして、
 mysqldump --default-character-set=latin1 -u root foo_production > foo.mysql

ダンプファイルの SET NAMES を書き換え
- /*!40101 SET NAMES latin1 */;
+ /*!40101 SET NAMES utf8 */;

database の再作成
mysql -u root -e "drop database foo_production; create database foo_production"

そして、ロード
mysql -u root foo_production < foo.sql


つまらないバッドノウハウで恐縮ですが、以前にも同じ事をやらかしてしまったことがあったので役に立つこともあるかと。

Wednesday, October 22, 2008

Passenger: Keeping ApplicationSpawner alive speeds up spawning an instance

アクセス頻度が少ないサイトで Passenger を使っている場合、しばらくしてからアクセスするとデプロイ直後や apache のリスタート直後と同じくらいレスポンスが遅くなることがあります。

Passenger は fork 時の copy-on-write により複数のアプリケーションプロセスが消費する実メモリのサイズが小さくなるようになっていて、その fork も以下のような3段階で行っているようです。

  1. 最初に起動する spawn server から fork して、 Rails をロード (framework spawner)

  2. framework spawner から fork して、アプリケーションをロード (application spawner)

  3. application spawner から fork して、リクエストを処理する過程で必要なファイルをオートロード (application instance)


spawn server は apache が終了するまで生きているようなんですけど、 framework spawner と application spawner にはタイムアウトする時間が設定されていて、 Passenger 2.0.3 のデフォルトではそれぞれ、30分、10分となっています。
したがって、前回のアクセスから10分過ぎると、再度 application spawner から生成し直すのでレスポンスに時間がかかります。

ということは、 application spawner のタイムアウト時間を十分に長くすればよいということで、無理矢理 passenger のファイルを変更。

/usr/lib/ruby/gems/1.8/gems/passenger-2.0.3/lib/passenger/constants.rb:
-   FRAMEWORK_SPAWNER_MAX_IDLE_TIME = 30 * 60
- APP_SPAWNER_MAX_IDLE_TIME = 10 * 60
+ # keep them alive for a week
+ FRAMEWORK_SPAWNER_MAX_IDLE_TIME = 7 * 24 * 60 * 60
+ APP_SPAWNER_MAX_IDLE_TIME = 7 * 24 * 60 * 60


Passenger 2.0.3 の段階では上記のような変更が必要ですが、 将来のバージョンでは apache の設定ファイルで RailsFrameworkSpawnerIdleTime, RailsAppSpawnerIdleTime を指定すれば変更できるようになるようです。

また、 Phusion Passenger users guide - 8.3 Capistrano Recipe の note に書いてあるように、デプロイ後に新しい application spawner が生成されても古いのが残ったままになるので、デプロイするときに kill。

config/deploy.rb:
namespace :passenger do
namespace :application_spawner do
desc "kill Passenger ApplicationSpawner"
task :kill do
run "kill $( passenger-memory-stats | grep 'Passenger ApplicationSpawner' | awk '{ print $1 }' ) || true"
end
after "deploy:update_code", "passenger:application_spawner:kill"
end
end

Passenger のドキュメントに書かれているように "Passenger spawn server" を殺すには root 権限が必要なので代わりに application spawner を kill。なんとなく不安定な気もするけど、これで様子を見ることにします。

参考資料:
Wishlist — passenger — GitHub
RailsFrameworkSpawnerIdleTime and RailsAppSpawnerIdleTime - Phusion Passenger Discussions | Google Groups
Modrails ... Slow first request ... - Phusion Passenger Discussions | Google Groups

Sunday, October 19, 2008

URL options for polymorphic_url

routes.rb:
 map.resources :users 

というリソースへのルートを定義している場合、

 polymorphic_url(:users) # => "/users"

という具合になります。

そこで、追加のパラメータをあたえると
 polymorphic_url(:users, :foo => 1) # => "/users?foo=1"

となることを期待する訳なんですけど、 Rails 2.1.1 ではそうはならなくて、URLパラメータが付かない一つ前の例と同じパスが返ってくる。

#880 URL options for polymorphic_url - Ruby on Rails - rails

edge では修正されてますけど、リリースするまでは、以下のような汚いパッチでごまかします。

if Rails::VERSION::STRING == "2.1.1"
# polymorphic_url should accept extra parameter options
# polymorphic_path(:projects, :foo => 1) # should be => /projects?foo=1
# This portion of code fixes this issue in a quick and dirty way.
ActionController::PolymorphicRoutes.module_eval do
def polymorphic_url_with_params(record_or_hash_or_array, options = {})
url = polymorphic_url_without_params(record_or_hash_or_array, options)
url_options = options.except(:action, :routing_type, :format)
unless url_options.blank?
url += "?" + url_options.to_param
end
url
end
alias_method_chain :polymorphic_url, :params

%w(edit new formatted).each do |action|
module_eval <<-EOT, __FILE__, __LINE__
def #{action}_polymorphic_url(record_or_hash, options = {})
polymorphic_url(record_or_hash, options.merge(:action => "#{action}"))
end

def #{action}_polymorphic_path(record_or_hash, options = {})
polymorphic_url(record_or_hash, options.merge(:action => "#{action}", :routing_type => :path))
end
EOT
end
end
end

Tuesday, October 14, 2008

Cloning a local git repository into a remote host

リモートリポジトリをローカルにクローンする方法は知っているけど、ローカルリポジトリのクローンをリモートに作る方法がいまいちわからなかったので調べてみました(より標準的な方法を知っているひとはコメントください)。

まずは、ローカルにベアリポジトリのクローンを作成してアーカイブ。
 $ git clone --bare -l foo/.git foo.git
$ tar czf foo.git.tar.gz foo.git

これをリモートにコピーして展開すればOK

あとは、ローカルの clone 元の .git/config に以下の行を追加
[remote "origin"]
url = ssh://name.of.host/path/to/repos.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master

git push はデフォルトで "origin" リポジトリのすべてのブランチを push するので、.git/config に remote "origin" のセクションがあれば、その url と fetch として記述されている refspec のマッピングを利用します。

また、 git pull はデフォルトで "origin" リポジトリから、 .git/config の branch セクションの remote が pull するリポジトリと一致するブランチを pull する (たぶん)。

よって、上記の設定により "git push", "git pull" だけで svn commit/update 相当の操作ができる。

ちなみに、これらの設定は
 git clone ssh://name.of.host/path/to/repos.git 
で得られるクローンリポジトリの設定と同じです。

Wednesday, October 8, 2008

Test-driven learning

Andrew Hunt のブログの記事 /\ndy - Test-driven Learning より。TDD (Test-Driven Development) から思いついたと書いてます。

それが最良の方法かどうかはわからないけど、やっぱり「習うより慣れよ」というのは効果があると思います。特に学習するコトが今までの経験から推測できなかったりするときには特に。今までの経験が活用できる場合は頭の中でテストがある程度できるけど、そうでない場合は実際に手を動かしてそれがどういう結果になるかどうかのフィードバックが得られないとなかなか進めない。

すいません、当たり前の事書いて...。