以前作ったモジュール(SpeedyCGI と module reload - daily dayflower)でたまにモジュールファイルの変更を検知できないことがあったんですが,理由がわかりました。
SpeedyCGI の挙動をおさらいすると,
- frontend が backend を探す。いればよし
- backend がいない場合 backend(0) を生成
- backend(0) がスクリプトをコンパイル
- backend(0) が fork して backend(1) を生成
- frontend が backend(1) と通信
- backend(1) が実行フェーズに入る
だいたいこんな感じ。
以前のモジュールだと backend(1) で各モジュールファイルの mtime を保持していて,変更があれば「リロードしてね」というメッセージを出力していました。
問題は backend(1) の生存 timeout(デフォルトで1時間だが変更可能)に達した時です。このとき backend(0) がすぐに死んでくれればいいんですけど,実際には 10 秒ほど猶予をもうけてから死ぬコードになってました。
src/speedy_backend_main.c からの抜粋ですが,
while (1) { int have_children; /* Unlock file */ speedy_file_set_state(FS_HAVESLOTS); /* Wait for sig from dead child or from frontend */ speedy_sig_wait(&sl); ... /* If no children, set an alarm */ alarm(have_children ? 0 : 10); }
子供(backend(1))がいない場合,10秒の alarm をセットして再びシグナル待ちに入るんですね。
だから,backend(1) が生存 timeout して消えて,backend(0) が死ぬまでの間約 10 秒の間にモジュールに変更をしたとしても,この時点でリクエストを送ると backend(0) は再び子供 backend(2) を fork して生成する。backend(1) が持っていた mtime の集合体が消えてしまうので変更の検知ができない,ということになります。
じゃあどうしよう?
いろいろ試行錯誤したんですが,backend(0) がコンパイルした時刻を覚えておいて,backend(1, 2, ...) が初回初期化した時刻と比較する,という泥臭い方法をとることにしました(もともと泥臭いロジックなんですが)。
具体的には,
CHECK { # backend(0) のコンパイル時に実行 my $current_time = time; *_check_time = sub { return $current_time }; } my $stale_condition; INIT { # backend(1) の初期化時に実行 $stale_condition = (time - _check_time() > 5); }
のようにして,$stale_condition が立った時には「タイムアウトしたんでリロードしたほうがいいんじゃないの?」とメッセージを出力することにする。これはこれでうざいですが,変更を検知できないよりはマシかな,と。