Module::Refresh とモジュールのダイナミックロードの相性
以前(⇒【永続化Perlでモジュールの再読み込み ⇒ Module::Refresh - daily dayflower】)の積み残し課題です。
main.cgi
が MyApp::Runner
を呼び,MyApp::Runner
が MyApp::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!'
このように変更が反映されました。
なぜこのようなことが起きるかというと,
- CGI の最初のほうで
Module::Refresh->refresh()
が呼ばれる - この時点では
Module::Refresh
に保存されている mtime キャッシュは,コンパイルフェーズで読み込まれたMyApp::Runner
のみ MyApp::App
をダイナミックロードするがModule::Refresh
の mtime キャッシュには登録されない- 直後((一度リロードしたりすると
%Module::Refresh::CACHE
にMyApp::App
の mtime が保存されるので,変更を検知できます))のリクエストでMyApp::App
が変更されていても変更を検出できない - このとき
Module::Refresh
のキャッシュにMyApp::App
の mtime が保存される - 次に
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::Inspector
の filename()
関数を使っています。
これで MyApp::App
を書き換えると,初回からでもきちんと変更が反映されるようになりました。
Module::Refresh
に load()
という関数を定義してもらうのも手ですし,UNIVERSAL::require::refresh
というモジュールをでっちあげるという手もありますが,今回は自作のローダ側に手をいれることにしました。
問題は,手を入れる前だとプロダクション環境で Module::Refresh->refresh()
を削除するだけでよかったんですが,こちらにも手をいれなきゃいけないことですね。