Module::Refresh とモジュールのダイナミックロードの相性

以前(⇒【永続化Perlでモジュールの再読み込み ⇒ Module::Refresh - daily dayflower】)の積み残し課題です。

main.cgiMyApp::Runner を呼び,MyApp::RunnerMyApp::App をダイナミックロードして実行し,戻り値を表示する CGI です。

CGI

#!/usr/bin/perl
use strict;

use Module::Refresh;

Module::Refresh->refresh();

use MyApp::Runner;

MyApp::Runner->run('MyApp::App');

ローダクラスは

package MyApp::Runner;

use strict;

sub load_module {
    my ($class, $target) = @_;

    eval "require $target" or die $@;
}

sub run {
    my ($class, $target) = @_;

    $class->load_module($target);

    my $output = $target->process();

    print {*STDOUT}
        "Content-Type: text/plain; charset=UTF-8\n",
        "\n",
        "Application said '$output'\n"
    ;
}

1;

UNIVERSAL::require 使えという気もしますが,これくらいのサンプルなのでまぁ eval でいいかな,と。後々の布石として load_module() というサブルーチンにわけてあります。

で,アプリケーションクラスは,

package MyApp::App;

use strict;

sub process {
    return 'Hello, world!';
}

1;

以上のプログラムを実行すると,

Application said 'Hello, world!'

と,出力されるわけです。


では,ダイナミックロードされる MyApp::App を書き換えてみましょう。

package MyApp::App;

... snip ...

sub process {
    return 'Hello, new world!';
}

... snip ...

実行結果は,

Application said 'Hello, world!'

あれ?変わりません。

じゃあもう一回変更してみると……

sub process {
    return 'Hello, not new world!';
}

今度は

Application said 'Hello, not new world!'

このように変更が反映されました。

なぜこのようなことが起きるかというと,

  1. CGI の最初のほうで Module::Refresh->refresh() が呼ばれる
  2. この時点では Module::Refresh に保存されている mtime キャッシュは,コンパイルフェーズで読み込まれた MyApp::Runner のみ
  3. MyApp::App をダイナミックロードするが Module::Refresh の mtime キャッシュには登録されない
  4. 直後((一度リロードしたりすると %Module::Refresh::CACHEMyApp::App の mtime が保存されるので,変更を検知できます))のリクエストで MyApp::App が変更されていても変更を検出できない
  5. このとき Module::Refresh のキャッシュに MyApp::App の mtime が保存される
  6. 次に MyApp::App が変更された時には変更を検知できる

という仕組みによります。

ということは,3 の時点で Module::Refresh の mtime キャッシュに保存されるようにすればよいのではないか,と。

package MyApp::Runner;

... snip ...

sub load_module {
    my ($class, $target) = @_;
    use Class::Inspector;

    eval "require $target" or die $@;

    my $file = Class::Inspector->filename($target);
    Module::Refresh->update_cache($file);
}

... snip ...

基本的に Module::Refresh の関数はモジュール名として 'MyApp/App.pm' という形((%INC のキーと同じですね))しか受け取らないので,Class::Inspectorfilename() 関数を使っています。

これで MyApp::App を書き換えると,初回からでもきちんと変更が反映されるようになりました。

Module::Refreshload() という関数を定義してもらうのも手ですし,UNIVERSAL::require::refresh というモジュールをでっちあげるという手もありますが,今回は自作のローダ側に手をいれることにしました。

問題は,手を入れる前だとプロダクション環境で Module::Refresh->refresh() を削除するだけでよかったんですが,こちらにも手をいれなきゃいけないことですね。