永続化Perlでモジュールの再読み込み ⇒ Module::Refresh

mod_perlPersistentPerl (a.k.a. SpeedyCGI) など Perl インタプリタを永続化させる仕組みのものは*1,一度読み込まれたモジュールは,その後ファイルが変更されたとしても自動的に再読み込みしてくれたりはしません。mod_perl では Apache(2)::Reload というモジュールがあり,そのような場合でもうまくとりはからってくれます。

で,かつて PersistentPerl でも同じようなことができないかと思って苦闘したことがあるのですが(SpeedyCGI と module reload - daily dayflower),最近ふと CPAN を漁っていて Module::Refresh なるものがあることを発見しました。Jesse プロダクトです。

This module is a generalization of the functionality provided by Apache::StatINC and Apache::Reload. It's designed to make it easy to do simple iterative development when working in a persistent environment.


It does not require mod_perl.

Module::Refresh - DESCRIPTION

ソースを読んでみましたがわりと素直。つか自分で ModPerl::Util::unload_package_pp() を参考に同じように書いていた頃はうまくいかなかったのに,これがうまく動くとは…格の違いをみせつけられました。

使い方は,PersistentPerl を例にすると(今後の例も PersistentPerl がターゲットです),メインスクリプト sample.cgi で,

#!/usr/bin/perperl

use strict;
use warnings;
use Module::Refresh;

use HogeHoge;
use FugaFuga;

Module::Refresh->refresh();

...通常の処理を書く...

のように,リクエストごとに Module::Refresh->refresh() を呼び出してやると,使用しているモジュール(HogeHoge や FugaFuga)に変更があった場合(mtime),自動的に再読み込みしてくれます。

さて,いろいろいじめてみることにします。

モジュール側で Module::Refresh を使う

メインスクリプトではなく,そこから使うモジュールで Module::Refresh を使った場合はどうなるか。

メインスクリプト

#!/usr/bin/perperl

use MyApp;

MyApp->run();

MyApp モジュール

package MyApp;

use Module::Refresh;
use HogeHoge;
use FugaFuga;

sub run {
    my $class = shift;

    print {*STDOUT} "Content-Type: text/plain\n\n";
    print {*STDOUT} "Hello, world!\n";
}

1;

このようなケースでも HogeHoge や FugaFuga に変更がある場合,問題なく再読み込みしてくれます。

ただし,MyApp 自体に変更を加えた場合,

[Tue Aug 07 17:30:29 2007] [error] [client 192.168.0.129]
  MyApp::run: Can't undef active subroutine
    at /usr/lib/perl5/site_perl/5.8.8/Module/Refresh.pm line 181.
[Tue Aug 07 17:30:29 2007] [error] [client 192.168.0.129]
  Subroutine run redefined at lib/MyApp.pm line 13.

のように警告が出力され,そのリクエスト自身にはモジュールの変更が反映されません。再度リクエストを発行すれば(ブラウザをリロードすれば)反映されます。このようなこともあるので,やはりメインスクリプトから呼び出すのが間違いが少ないです。

Class::Data::Inheritable なクラスで大丈夫?

この手の永続化/再読み込み機能は INIT フェーズがうまく実行されなかったりする都合上,Class::Data::Inheritable 等と相性が悪いことがあります(see Class::Data::Reloadable)。

てことで,ClassA として

package ClassA;

use base qw( Class::Data::Inheritable );

__PACKAGE__->mk_classdata( message => 'base message' );

1;

を書き,ClassB として ClassA を継承して

package ClassB;

use base qw( ClassA );

__PACKAGE__->message('overridden message');

1;

のようにして,MyApp::run() を

use ClassB;

sub run {
    print {*STDOUT} "Content-Type: text/plain\n\n";
    print {*STDOUT} ClassB->message;
}

とします。

ClassA や ClassB を書き換えてみましたが,きちんと再読み込みされ,message にはきちんと値が設定されていました。なんだうまくいってるじゃないか。mod_perl じゃなくて PersitentPerl だからかな?

シンボルテーブルまわりで意地悪する

ClassB が ClassA 自体を拡張するようなモジュールにしてみます。

ClassA として

package ClassA;

sub target {
    return 'world';
}

1;

ClassB として

package ClassB;

use ClassA;

sub ClassA::say {
    my $class   = shift;

    my $target = $class->target;

    return qq|Hello, ${target}!|;
}

1;

のようにします。なんとも行儀が悪いですが。

MyApp として

use ClassA;
use ClassB;

sub run {
    print {*STDOUT} "Content-Type: text/plain\n\n";
    print {*STDOUT} ClassA->say;
}

のようにします。

再読み込み機能はシンボルテーブルをいじるため,ClassA を変更すると ClassB によって追加されたメソッドが消えたりしうるのですが……

これもうまくいきました。ClassA を touch してもきちんと say メソッドは残っています。

シンボルテーブルまわりでもっと意地悪する

ClassB で ClassA::target() を再定義してみます。

package ClassB;

{
    no warnings 'redefine';

    sub ClassA::target {
        return 'overridden world';
    }
}

...snip...

sample.cgi を実行すると,'Hello, overridden world!' のように表示されます。

ではここで touch ClassA.pm すると…… ClassA::target() が ClassA のものに再定義され,'Hello, world!' に戻ってしまいました。仕方ないというか当たり前な話ですが。

まとめ

PersistentPerl と Module::Refresh の組み合わせはなかなか相性がいいみたいです。ただ,当然のことながらシンボルテーブルをばりばりいじるとうまくいかないことがあります。なので昔とったアプローチ*2もまあ悪くもないかな,と。でも総合的にみて今回の Module::Refresh のほうがお気楽だし CPAN module なので安心ですね。

積み残し

ダイナミックにモジュールをロードする場合にどうなるかを検証するのを忘れてました(⇒調べました;Module::Refresh とモジュールのダイナミックロードの相性 - daily dayflower。あと,(「*foobar = ...」みたく)明示的にシンボルテーブルをいじってないのでそのへんも積み残し。

*1:FastCGI だとどうなのかは知りません

*2:親プロセスを kill SIGTERM し PersistentPerl->shutdown_now() する