SpeedyCGI のプロセスの癖

以前作ったモジュール(SpeedyCGI と module reload - daily dayflower)でたまにモジュールファイルの変更を検知できないことがあったんですが,理由がわかりました。

SpeedyCGI の挙動をおさらいすると,

  1. frontend が backend を探す。いればよし
  2. backend がいない場合 backend(0) を生成
  3. backend(0) がスクリプトコンパイル
  4. backend(0) が fork して backend(1) を生成
  5. frontend が backend(1) と通信
  6. 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 が立った時には「タイムアウトしたんでリロードしたほうがいいんじゃないの?」とメッセージを出力することにする。これはこれでうざいですが,変更を検知できないよりはマシかな,と。