2008/02/03(日)SpeedyCGI/FastCGI でモジュール動的更新
Satsuki-system(adiaryの基礎)で使用している、モジュールの自動更新機構と SpeedyCGI/FastCGI での実装について詳細に解説します。
モジュール自動更新の必要性
SpeedyCGIやmod_perl等々のスクリプトキャッシュ環境で実行速度が改善するのは、プログラム本体やライブラリをコンパイルした状態で内部に保持するためスクリプト実行時間の大半を占めるコンパイル時間が短縮されるというメリットがあるためです。特に最近のスクリプトは便利なライブラリを使用する傾向が強く、実行時間の95%以上がスクリプトのコンパイル時間なんてこともめずらしくありません。*1
しかしそのような環境では、スクリプトライブラリを書き換えてもプログラムは依然としてキャッシュした古いライブラリを使用し続けるので、少し書き換える度に SpeedyCGI の常駐プロセスを手動で殺したり、Apacheを再起動させたりといった手間が必要になります。
これを避けるためにはスクリプト自身でライブラリの更新をチェックし、ライブラリが更新されていた際に自動的にスクリプトを再起動(リロード)させる仕組みが必要になります。
ライブラリ更新チェック方法
Satsuki-system(adiary) では lib/Satsuki/Base_auto2.pm にその仕組みが実装されています(むしろそのファイルは、それ関連しか書かれてません)。
# AGPLv3
#######################################################
# ■SpeedyCGI / FastCGI用のリロードチェックサブルーチン
#######################################################
my %lib_modtime;
#------------------------------------------------------
# ●新たにロードされたライブラリの更新時間を保存
#------------------------------------------------------
sub save_lib_modtime {
while (my ($pkg, $file) = each(%INC)) {
if (exists $lib_modtime{$file}) { next; }
$lib_modtime{$file} = (stat($file)) [9];
}
}
#------------------------------------------------------
# ●ライブラリが更新されているか確認
#------------------------------------------------------
sub check_lib_update {
while(my ($file, $current_tm) = each(%lib_modtime)) {
my $tm = (stat($file)) [9];
if ($tm > $current_tm) {
return 1; # 更新されている
}
}
return 0;
}
%lib_modtime がライブラリの更新時刻を保持するハッシュです。save_lib_modtime をプログラムの終了時に呼び出し、新たにロードされたライブラリを %lib_modtime に保存しています。そしてプログラム開始時に check_lib_update を呼び出して、更新されたライブラリがあるか確認しています。
もし更新されたライブラリが発見された場合は、この実行終了後に(キャッシュされた)プロセスを終了する予約をして、リクエストされたURIにリダイレクトします。*2
#SpeedyCGI
if ($self->check_lib_update()) {
CGI::SpeedyCGI->shutdown_next_time(); # リクエスト終了後シャットダウン
if ($ENV{REQUEST_METHOD} ne 'POST') { # POSTはリダイレクトできないのでそのまま処理
$self->redirect($ENV{REQUEST_URI}); # 同一URLにリダイレクト
}
}
FastCGIでの処理も同様です。mod_perlの場合はApache自体を reload させるわけにはいかないので、別の方法になっています。
なぜか更新が検出できない
SpeedyCGI のバグなのか仕様なのか、Base.pm の更新が検出できない(リロードされない)というバグに長いこと悩まされていました。SpeedyCGI は何分かアクセスがなければ自動でプロセスが落ちるためさほど深刻ではありませんが、奇妙なので少し調査してみました。
SpeedyCGIの起動ルーチンは次のようになっています。
#!/usr/bin/speedy
use Satsuki::Base();
{
# ルートオブジェクト生成
my $ROBJ = Satsuki::Base->new();
# SpeedyCGI向けリロードチェック
$ROBJ->init_for_speedycgi('Satsuki::');
# メイン
$ROBJ->start_up();
}
色々調べてみると、CGI::SpeedyCGI->shutdown_next_time(); 自体の処理は問題なく、SpeedyCGI自体はリロードがかかっているにも関わらず、Satsuki::Base がリロードされないということが判明しました。どうやらshutdown_next_time() は本当にリロードしているわけではなく、use段階は再実行しない(perlのBEGIN構文)ようです*3。
試行錯誤の末、use Satsuki::Base(); の部分を次のように書き換えることで解決されました。
if (! $Satsuki::Base::VERSION) {
delete $INC{"Satsuki/Base.pm"};
require Satsuki::Base;
}
ここに delete が付いている理由ですが、Base.pm のロードの途中に構文エラーなどで失敗した場合、requireしたライブラリにロード済の情報が残ったまま($INC{"Satsuki/Base.pm"}が保存されたまま)の状態でキャッシュしてしまいます。この状態ではライブラリのリロードに問題があるため、ロード情報を消してから require します。