以前(⇒【永続化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() を削除するだけでよかったんですが,こちらにも手をいれなきゃいけないことですね。