読者です 読者をやめる 読者になる 読者になる

北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

レガシーオレオレフレームワークをPSGI化した記録

perl+web

5年ほど前に作ったレガシーな自作フレームワークを、この度PSGI化した際の記録です。単なる記録であって、必ずしも「PSGIへの対応はこうやるといいよ!」いう内容ではありません*1が、興味があればどうぞ。

レガシーオレオレフレームワークの概要

Oreore::Application はCGI::Applicationのように、1リクエストに対して1インスタンスフレームワークです。各 App.pm に 呼び出し用の hogehoge.pl を1つずつ割り当てて、 Apache::Registryで運用されていました。

package MyApp;
use base qw(Oreore::Application);

# For index.pl?action=index
sub action_index{
    my $self = shift;
    $self->response->content( 'NAME: ' . $self->request->param( 'name' ) );
}

# For index.pl?action=other
sub action_other{
    my $self = shift;
    # .. 略 ..
}

1;

これを呼ぶindex.plは以下です。

#!/usr/local/bin/perl
use MyApp;

my $app = MyApp->new();
$app->run();

PSGI化する必要があるのか

これは大事な問いです。無理にリスクを冒してPSGI化する必要はないと思います。

今回の改修の発端は、このフレームワークが mod_perl1.x に密結合をしているのを mod_perl2 に移す必要が出てきた、というものです。このような作業は何度もしたくないので、 PSGI へ対応させてバックエンドサーバの変化に柔軟に対応させることにしました。

書き換え方法の考察

こんなレガシーなコードは全部書き直せ! と、言いたいのですが、このフレームワークを使ったアプリが大量にある状況で、出来ればサーバとフレームワークの側で全て吸収したいと言うのが本音です。CGI::Application::PSGIでは、App.pmは書き換えずに各index.plをapp.psgiへ書き換える方法をとっています。しかし、現状抱えている *.pl が100個近くあったので、Oreore::Application の書き換えでは*.pl側は触らずに済ませることにします。

最終的には、以下の方針をとりました。

  • *.pl が、全てPSGIファイル*2となるように Oreore::Application を改修
  • *.pl (それぞれがPSGIファイル)を、Apache::Registry と似たような感じでサーブさせる

PSGI対応のベースとしては、セオリー通りPlackを利用します。

Oreore::Application の改修

幸いなことに、すべての *.pl は App->new->run という形をとっています。レガシーフレームワークではrunをするとコードが実行されていたのを、runがPSGIアプリケーションを返すように変更すればよさそうです*3

と言うことで、改修前は、

# 改修前のOreore::Application
sub new{
    my $class = shift;

    bless { 
        request  => Oreore::Request->new,
        response => Oreore::Response->new,
    }, $class;
}

sub run {
    my $self = shift;
    $self->_run_impl;
    $self->response->flush;
}

という感じだったコードを、

# PSGI化したOreore::Application
sub new{
    # $app->new->run の時は何もしない。runの中で$envを元にインスタンス化する
    return $_[0] if @_ == 1;

    my ($class, $env) = @_;
    bless { 
        request  => Oreore::Request->new( $env ),
        response => Oreore::Response->new,
    }, $class
}

sub to_psgi{
    my $class = shift;
    return sub {
        my $env = shift;

        my $app = $class->new( $env );
        $app->_run_impl;

        return $app->response->flush;
    };
}

sub run {
    my $class = shift;
    return $class->to_psgi;
}

という風に変更しました。RequestやResponseは中身がApache::*だったのをPlack::*に書き換えています。

ここで大きく変わったのは、Oreore::Applicationクラスに引数と戻り値ができたことです。改修前はパラメータは環境変数や標準入力から入ってきていましたが、改修後はコンストラクタから渡された$envをPlack::Requestにパースさせる流れに変わっています。また、$response->flushでは蓄えたヘッダやcontentを$r->printする代わりに、 Plack::Response->finalize を呼び元へ返すようになりました。

ちなみに、newの中身がキモいのは *.plに書かれている $app->new->run をそのまま使おうとしたために出てきた歪みです。$envを受け取るためにrunの中でnewを行う必要があるにも関わらずrunの前にnewがあるので、このnewをスルーさせてます。

*.pl のサーブ

今回の発端はmod_perl2へ対応させることだったので、Plack::Handler::Apache2::Registryを利用するのがベストでしょう。

ただ、せっかくなら流行りのStarmanを使いたいなあと言うのがオタク心ってもんです。最上位に以下のようにPlack::App::PSGIBinを使った app.psgi を置くと、点在する *.pl をStarmanを使って動かすことができます。

use Plack::App::PSGIBin;
use File::Basename;

Plack::App::PSGIBin->new( root => dirname( __FILE__ ) )->to_app;

その他、微調整

他、以下のような微調整を行いました。

単体テストを書く

レガシーなフレームワークにはテストがなかったので、Plack::Testでテストを書きまくりました。おかげで、フレームワークの動作で曖昧だった部分がすべてクリアになりました。

カレントディレクトリの設定

Apache::Registry は *.pl に対してカレントディレクトリを設定してくれますが、今回の方法だとこれを設定してくれません*4。MiddleWare を作れば対応できるでしょう。

use File::chdir;

sub to_psgi{
    my $class = shift;
    my $dir = dirname( __FILE__ );

    builder {
        enable sub {
            # カレントディレクトリを調整するMiddleware
            my $app = shift;
            return sub {
                my $env = shift;
                local $CWD = $dir; 

                return $app->( $env );
            };
        };
        sub {
            my $env = shift;

            my $app = $class->new( $env );
            $app->_run_impl;

            return $app->response->flush;
        };
    };
}
StackTraceを少し便利に

plackup を -E development で起動するとついてくるPlack::Middleware::StackTraceは大変便利ですが、このMiddlewareは最後に発生したdieを追跡するため、フレームワーク内で try ... catch ... die をしていると、原因となった部分ではなく rethrow させた部分のトレースが表示されてしまいます。

現行のPlack::Middleware::StackTraceの実装に完全に依存したその場凌ぎのhackですが、以下のような die_without_trace を作って try ... catch ... die_without_trace をすれば、この問題を回避して有用な StackTrace を表示させることができます。

sub die_without_trace{
    local $SIG{__DIE__} = $SIG{__DIE__};
    delete $SIG{__DIE__}; # for M::StackTrace not to record this dieing.
    die @_;
}
*.pl を直接起動できるようにする

この改修で$app->runはPSGIを返すだけになってしまいましたが、本来であればやはりrunを実行するとアプリが走って欲しいものです。runを以下のように書けば、他のサーバ等からロードされた場合はPSGIアプリを返し、*.plが直接実行された場合はアプリを走らせるというハイブリッド型のファイルにすることができます。

sub run {
    my $class = shift;
    my $run_script = ( caller )[1];
    my $app = $class->to_psgi;

    if( $0 eq $run_script ){
        # .pl ファイルが直接実行されていればPlackによって立ち上げる
        Plack::Loader->auto( port => 5000 )->run( $app );
        return;
    }else{
        return $app;
    }
}

さらに、 Plack::Loader->auto はCGIとして呼び出された場合には自動で Plack::Handler::CGI を使って動かしてくれるので、この Oreore::Application を使って書かれた *.pl は、以下に挙げる全ての呼び出しに対応しています。

  • PSGIアプリケーションファイルとしての起動
  • ./hoge.pl と直接実行
  • CGIスクリプトとしての起動
    • よってもちろん、 Apache::Registry からの起動も*5

まとめ

オレオレフレームワークPSGI化によって、以下のような恩恵がありました。

  • 様々な環境でアプリを動かすことができるようになった(元々やりたかったのはこれ)
    • Plack::Handler::* やstarman, starlet, twiggyを利用する。
  • 単体テストが簡単に書けるようになった
    • test_psgi でバンバン書ける
  • 開発が容易になった
    • Plack::Middleware::StackTrace で簡単にエラーをトレースできる
    • plackup で、mod_perlがなくても任意の *.pl を開発者のPCで動かすことが可能
  • 今後はPSGI準拠のMiddlewareをそのまま使える

逆に、解決されなかったこともありました。

  • PSGIでも構成はレガシーなまま(レガシーな資源を残す方針だったので)
  • パフォーマンスには、大きな変化はなし*6
  • フレームワーク側の実装は複雑になった

今回はRequestとResponseが抽象化されていたのでPlack::Requestを使う流れで対応しましたが、よりCGIに近ければCGI::PSGICGI::Emulate::PSGIを使うなど、様々なアプローチが考えられます。CPANに上がっているモジュールを眺めて方針を決めた方がよいでしょう。

*1:うちのような環境は、そもそも少ないので適用できないのではないかと思うw

*2:PSGI仕様的には"PSGIファイル"ってものはないですが、ここで言うのはplackupで使うapp.psgi のような PSGI Application を返すファイル、の意味です。

*3:runの名前とその機能が食い違ってしまうという問題はありますが、そこは許容範囲であるとしました。

*4:後、必要であれば local $0 も設定しないと、*.plのファイル名にはなっていません

*5:実際の改修では、実は Plack::Handler::CGI + Apache::Registry で動くバージョンを先に作ったのですが、本エントリでは逆順で説明しました。

*6:ただし、mod_perlStarmanに変えればその恩恵は受けられます