mod_perl 2.0.2 へのマイグレーション

mod_perl 2 が Stable リリースになって気がつけば半年以上経った様子。はてなではこれまで mod_perl 2 は mod_perl 2.0RC-4 (1.99) とかを使ってましたが、ぼちぼち 2.0 にちゃんと移行した方がいいかなと、重い腰を上げつつ作業してます。

現在、mod_perl には互換性のない三つのバージョンが存在してます。

1.0 は Apache 1.3 の API に対応している mod_perl、1.99 と 2.0 は Apache 2.0 API に対応している mod_perl です。Apache 2.0 がそれまでのバージョンとの API の互換性を捨ててアーキテクチャの見直しが行われたのをきっかけに、mod_perl後方互換性を捨ててスクラッチから書き直された結果の産物です。

1.99 というのは、mod_perl 2.0 が Stable リリースされる直前までのバージョンで、言うなればベータ版。ベータが取れるときにいくつかの API が変更になったり、名前空間Apache から Apache2 に変更されたりといった経緯があって、その両者はだいたい同じなんだけど互換性がありません。なので、1.99 を使ってたアプリケーションが動いてるサーバーを 2.0 にアップデートするとそのアプリケーションは動かなくなってしまいます。

例えば Catalyst とかは Catalyst-Engine-Apache の中で、MP19、MP20、MP13 と三つの Engine を用意してその差異を吸収していたりします。

さて、みなさんご存じの CGI.pm は、CGI という名前の割には mod_perl で動くと mod_perlAPI を使ったりするようにソースが書かれているんですが、その最新版には

if (exists $ENV{MOD_PERL}) {
  # mod_perl handlers may run system() on scripts using CGI.pm;
  # Make sure so we don't get fooled by inherited $ENV{MOD_PERL}
  if (exists $ENV{MOD_PERL_API_VERSION} && $ENV{MOD_PERL_API_VERSION} == 2) {
    $MOD_PERL = 2;
    require Apache2::Response;
    require Apache2::RequestRec;
    require Apache2::RequestUtil;
    require Apache2::RequestIO;
    require APR::Pool;
  } else {
    $MOD_PERL = 1;
    require Apache;
  }
}

というコードがあります。mod_perl のバージョンを評価して、対応するバージョンのモジュールをロードしています。しかし、Catalyst の場合とは違って 1.99 には対応してくれてません。少しまえのバージョンまでは 1.0 と 1.99 に対応したコードになってましたが、ここ最近のアップデートで上記のコードに入れ替わりました。なので、CGI.pm を使ってる mod_perl 1.99 なコードは、CGI.pm のバージョンアップと共に動かなくなってしまうという罠があったりします。

1.99 は mod_perl のサイトにダウンロードのリンクがないし、この CGI.pm の事情なども見るに、もうおまえらいい加減 2.0 使えよなっていう開発コミュニティの意向なのかなあという感じがして、2.0.2 へ移行しようと作業をしているというわけであります。ついでなので、CGI.pm とか使ってる悪しき習慣をやめて Apache2::Request を使うようにとかフレームワークの改良もしてます。

さて、いざ作業しようと思ったわけですが実は僕は mod_perl API を直接使うときは 1.0 ばかりいじってて、 mod_perl 2.0 にはあんまり詳しくないけどフレームワークが抽象化してくれてるのをいいことにその辺がおざなりになっていました。のでこの正月にその辺りのドキュメントを漁っていました。

1.0 → 2.0 では、それほど大きく変わっていなくて苦労というほどのものはなかったのですが、1.0 のときと同じノリでやってるとはまったりする箇所はやっぱりあります。例えば 1.0 のときは

package Sandbox::Hello;
use strict;
use warnings;
use Apache::Constants qw(OK);

sub handler ($$) {
    my ($class, $r) = @_;
    $r->send_http_header('text/html');
    $r->print('Hello, World!');
    return OK;
}

1;

と、割と深いことを考えなくても handler() を定義して $rAPI を使って終了という具合でしたが 2.0 では $r に相当するオブジェクトのメソッドが複数のモジュールに分散していて、その対応をまず把握する必要があります。

  • Apache2::RequestIO
  • Apache2::RequestRec
  • Apache2::RequestUtil
  • Apache2::Server
  • Apache2::ServerUtil

この辺。それぞれのモジュールをロードすると、$r で扱える API が増えるので、それを使ってプログラミングしていきます。あと、send_http_header がなくなってるとか、OO でハンドラを書きたければ attribute で method を指定しなさいとかという細かい変更もあります。結果 Hello, World は

package Sandbox::Hello;
use strict;
use warnings;

use Apache2::ReqeustRec();
use Apache2::RequestIO();
use Apache2::Const -compile => 'OK';

sub handler : method {
    my ($class, $r) = @_;
    $r->content_type('text/plain');
    $r->print('Hello, World!');
    return Apache2::Const::OK;
}

1;

という案配になります。

このメソッドの変更とか、所望のメソッドがどこのモジュールに定義されてるかとかは ModPerl::MethodLookup を使って調べられるようになっていて

$ perl -Mmod_perl2 -MModPerl::MethodLookup -e print_method send_http_header
'send_http_header' is not a part of the mod_perl 2.0 API
use 'content_type' instead. To use method 'content_type' add:
        use Apache2::RequestRec ();

こんな感じでコマンドラインから都度調査しつつマイグレーションしていく形になります。ちなみに、API とモジュールの対応とかめんどくさかったら startup.pl で

use ModPerl::MethodLookup;
ModPerl::MethodLookup->preload_all_modules();

とすることもできます。

The function preload_all_modules() preloads all mod_perl 2.0 modules, which implement their API in XS. This is similar to the mod_perl 1.0 behavior which has most of its methods loaded at the startup.

だそうです。

という具合でぼちぼちやって、既存のアプリケーションはだいたい動くようになりました。

これで mod_perl 2 で追加された機能、例えば(Apache::Filter とか使わないでもOKな)I/O フィルタリングやハンドラのフック箇所の指定機能とかも安心して使えるし、サーバーやモジュールのバージョンアップにも気を遣わなくて済みそうです。prefork と worker でどれぐらい差がでるかのベンチマークとかもやってみたい。