SpeedyCGI と module reload

SpeedyCGI で遊んでました。mod_perl ほど設定に煩わされることもないし,ライブラリパスの局所化も容易なんで実験用としてはなかなか楽しいです。

ですけど,

How can I make sure speedy restarts when I edit a perl library used by the CGI?
Do a touch on the main cgi file that is executed. The mtime on the main file is checked each time the front-end runs.

ありがち。でもそれめんどくさいよねぇ。

ということで,Apache2::Reload みたいなのを作ろうと思っていろいろ試していたんですが一筋縄ではいきませんでした。


どうも SpeedyCGI の処理の流れが(コードの分析からじゃなくて現象からの分析なので適当)

  1. フロントエンド(mod_speedycgi や speedy)が CGI スクリプトを起動しようとする
  2. もし下記の条件にあてはまればバックエンドを再利用する
  3. さもなければバックエンドを新規作成する
  4. 作成されたばかりのバックエンドは CGI スクリプトをパースする
  5. バックエンドは子を fork し
  6. 子はインタプリタ処理を開始し,結果をフロントエンドに投げる

形になってるんで CGI::SpeedyCGI->shutdown_now() しても 6 の子が死んで4 のバックエンドが fork するだけなのです。つまり,この時点でスクリプトの再パースは行われない,と。

ということで ModPerl::Util::unload_package_pp(←これはこれでなかなかの力技)を参考に書いてみてもうまくいかない。


で,結果的にどうしたかというと

  1. %INC のパッケージが更新されていなければスルー
  2. 「リロードしてね」というメッセージを出力する
  3. getppid してバックエンド親プロセス(↑でいう 4)を kill TERM する
  4. 自分は CGI::SpeedyCGI->shutdown_now する

というシンプル?な形でうまくいきました。子から親に氏ねというのもあれですがうまく動いたのでよしとします。


コンセプトモデルとしては

package SCGI::FakeReload;

use strict;
use CGI::SpeedyCGI;

my %mtime;

sub _modified {
    while (my ($pkg, $file) = each %INC) {
        my $mt = (stat $file)[9];

        if (exists $mtime{$pkg}
         && $mtime{$pkg} > 0
         && $mt > $mtime{$pkg}) {
            return 1;
        }

        $mtime{$pkg} = $mt;
    }

    return 0;
}

sub check {
    if (_modified()) {
        print "Content-Type: text/plain\n\nReload !\n";
        kill 15, getppid;
        CGI::SpeedyCGI->shutdown_now();
        # NOTREACHED
    }
}

1;

で,CGI スクリプト

use SCGI::FakeReload;

SCGI::FakeReload::check();

って書いとけよみたいな感じですが,いちいち check() 実行するのもうざいんで,私家版では手を加えて require するだけで済むパッケージにしてます。