モダンPerlの世界へようこそ

第6回Catalyst::Upgrading:検証はお早めに

3年前の大混乱

モダンPerl界を代表するウェブアプリケーションフレームワークといわれるCatalystが2006年半ばに5.6系統から5.7系統に移行したとき、創始者のゼバスティアン・リーデル氏を追い出す形で集団管理体制に移行した開発チームが最初にしたことは、プロジェクト開始当初から使われてきたCatalystという名のディストリビューションはそのままに、Catalyst-Runtimeという新しいディストリビューションをつくることでした。

このようなディストリビューション名の変更は、CPANクライアントを使っている分には(内部でモジュール名からディストリビューション名への変換が行われるので)問題にならないのですが、外部のパッケージ管理者たちには少なからぬ負担をかけました。なにしろ突然100を越す関連パッケージの依存が変更になるのです。基本的にはメタ情報だけ書き換えれば済む話とはいえ、従来のCatalystパッケージと新しいCatalyst-Runtimeパッケージは当然コンフリクトの対象になりますし、場合によっては、どちらが入っていてもいいという立場をとるCPANモジュール側のメタ情報と、どちらか(ふつう新しい方)しか認めないというパッケージ側のメタ情報の齟齬が生じることもありえます。まっとうな管理者であればテストもやり直すことでしょう。

ただ、この変更には、うっかりアップデートしたせいでいきなり5.6系列から5.7系列に移行させられてしまうことはない(芋蔓式に移行させられそうになってもコンフリクトが発生するので検出できる⁠⁠、というメリットもありました。

なにしろ集団管理体制に移行した最大の理由は、Catalystを仕事で使い始めていた人たちがリーデル氏のたび重なる仕様変更に不満を爆発させたことだったのです。たえずCPANから最新版をインストールし続けることに抵抗を感じない人はともかく、サーバを増強したら突然互換性のないバージョンがインストールされた、というのでは困ります。十分な検証が済むまで(あるいは十分な検証が済んでからも)安心して旧版のパッケージを使い続けることができるようにしておくのは、特にさまざまな環境で開発を行わなければならない人にとっては大事なことでした。

Catamooseへの移行は待ったなし

それからそろそろ3年がたち、先日、とうとう5.8系列の正式版がリリースされました。

開発版は2008年10月から公開されていましたから、なかにはすでに試された方もいるかと思いますが、このCatalyst 5.8、俗称Catamooseでは、2005年にCatalystがMaypoleというフレームワークから分離独立したときからずっと使われてきた、Catalystの根幹をなすいくつかのモジュールが、Mooseに代表されるよりモダンなモジュールで差し替えられているのが最大のポイントです。

連載第2回でも紹介したように、たしかにこれらのモジュールには問題点もありました。突っ込んだ使い方をすると不可解な挙動に悩まされたりもしましたから、その意図自体は悪くなかったのですが、なにしろ土台の部分を取り替えようというのですから、いくら後退テストを充実させたところで、古いアプリケーションのなかには壊れてしまうものも出てきます。

今回の移行はまだ始まったばかりですから戦略の正否を評価できる段階にはありませんが、今回は、前回とは違って、ほかのパッケージをアップデートしただけのつもりでいたら、芋蔓式に新しいCatalystがインストールされて、古いアプリケーションに影響が出る、ということも十分起こりえます。

今回は筆者が実際に遭遇した問題を中心に、Catalyst 5.8に移行したときに起こりうる問題とその解決策を紹介していきましょう。

継承ツリーの操作にはご用心

伝統的に、Catalystアプリケーションのひな形は(コメントを除くと)このような形になっていました。

package MyApp;

use strict;
use warnings;
use Catalyst::Runtime '5.70';
use Catalyst qw/-Debug ConfigLoader Static::Simple/;

our $VERSION = '0.01';

__PACKAGE__->config( name => 'MyApp' );;
__PACKAGE__->setup;

1;

このMyAppは、Catalyst.pmを継承したコンテキストオブジェクト($c)であり、Catalyst::Controllerを継承したコントローラコンポーネントでもあるのですが、このようにuseしただけでCatalystがMyAppの継承ツリーに入ってしまうのはどうなのか、という議論があったため、2008年4月にリリースされたCatalyst::Devel 1.05以降、parentという新しいプラグマを利用して、Catalystを明示的に継承ツリーに追加するようになりました。1.05の書き方はすぐにあらためられてしまったので、1.06のひな形を紹介すると、このような感じになります。プラグインのロードがsetupのところに移動しているのがポイントです。

package MyApp;

use strict;
use warnings;
use Catalyst::Runtime '5.70';
use parent qw/Catalyst/;

our $VERSION = '0.01';

__PACKAGE__->config( name => 'MyApp' );;
__PACKAGE__->setup(qw/-Debug ConfigLoader Static::Simple/);

1;

Catalyst 5.8でも、このようなひな形通りの書き方になっていれば、新旧ともに問題ありません。

ところが、ここで両者を組み合わせてこのようなMyApp.pmを書くと、Catalyst 5.8ではエラーになります。

package MyApp;

use strict;
use warnings;
use Catalyst::Runtime '5.70';
use Catalyst qw/-Debug ConfigLoader Static::Simple/;
use parent 'Catalyst';  # または push @ISA, 'Catalyst';

our $VERSION = '0.01';

__PACKAGE__->config( name => 'MyApp' );;
__PACKAGE__->setup;

1;

おもしろいことに、この場合はuse parentをuse baseに変えるとエラーが消えるのですが、MooseベースになったCatalyst 5.8は、メソッド解決にC3を利用しているせいもあって、コンポーネントをuseしたあとに継承ツリーをいじられることを極度に嫌います。

この場合は古いやり方にしたがってuse parentの行を削除するか、現行のCatalyst::Devel 1.12がしているように、use parentの行をuse Catalystの上にもっていく、あるいは、旧版との互換性を考えなくてもよいなら、次に紹介するMoose的なやり方に変えてもよいでしょう。baseプラグマやparentプラグマを利用するときは、なるべくMooseの影響が出ないように、先に持っていくのが無難です。

use Mooseしたらextendsで拡張

ただし、いくら先に持っていこうとしても、そうはできない場合というのも存在します。たとえば、これはCatalyst 5.7系列でもそうなのですが、Mooseを使えばuse strictなどは不要になるからと思ってこのような書き方をすると、先ほどのトラップにひっかかります。

package MyApp;

use Moose;
use Catalyst qw/-Debug ConfigLoader Static::Simple/;

our $VERSION = '0.01';

__PACKAGE__->config( name => 'MyApp' );;
__PACKAGE__->setup;

1;

こちらも根本的な原因は同じなのですが、この場合はuse parentなどを使っても問題の解決にはなりませんので、MooseのAPIを使ってください。

package MyApp;

use Moose;
extends 'Catalyst';

our $VERSION = '0.01';

__PACKAGE__->config( name => 'MyApp' );;
__PACKAGE__->setup(qw/-Debug ConfigLoader Static::Simple/);

1;

なお、これは一般的なコンポーネントでも同様なのですが、コントローラについては、LocalやPath、Chainedといったディスパッチャ用のアトリビュートがうまく動かなくなる問題があるので、extendsの宣言をBEGINブロックに入れておくのが無難です。

package MyApp::Controller::Root;

use Moose;
# use base 'Catalyst::Controller'; # エラー
# extends  'Catalyst::Controller'; # アトリビュートを使わないモデルならOK
BEGIN { extends 'Catalyst::Controller'; }  # もっとも無難なやり方

1;

setup時のdeep recursion

それから、筆者が関係したアプリケーションでは見つかりませんでしたが、MyAppのなかでNEXTやnext::methodを使ってsetupを拡張していた場合は、Catalyst 5.8に移行するとdeep recursionが発生します。

package MyApp;

use strict;
use warnings;
use Catalyst;

our $VERSION = '0.01';

__PACKAGE__->config( name => 'MyApp' );
__PACKAGE__->setup(qw/-Debug/);

sub setup {
  my $self = shift;
  $self->next::method(@_);

  # DBに接続したりディレクトリを作成したり……
}

1;

この場合は、setupメソッドではなく、Catalyst 5.8で新たに用意されたsetup_finalizeメソッドにフックするよう書き直すと解決します。

package MyApp;

use strict;
use warnings;
use Catalyst;

our $VERSION = '0.01';

__PACKAGE__->config( name => 'MyApp' );
__PACKAGE__->setup(qw/-Debug/);

sub setup_finalize {
  my $self = shift;
  $self->next::method(@_);

  # DBに接続したりディレクトリを作成したり……
}

1;

より詳細なコントロールが必要な場合は、MooseのAPIを利用して次のように書き直すこともできます。この書き方はこの連載では説明してきませんでしたが、この場合はsetup_finalizeを実行したあとで、指定した無名関数の中身を実行しなさい、ということです。

package MyApp;

use Moose;
extends 'Catalyst';

our $VERSION = '0.01';

__PACKAGE__->config( name => 'MyApp' );
__PACKAGE__->setup(qw/-Debug/);

after setup_finalize => sub {
  my $self = shift;

  # DBに接続したりディレクトリを作成したり……
};

1;

don't use NEXT

これはアプリケーションが立ち上がらなくなるような問題ではないのですが、Catalyst 5.8以降、use NEXTしている古いコンポーネントを利用しているアプリケーションではこのような警告が出るようになっています。

MyApp::Model::DB is trying to use NEXT, which is deprecated. Please see the Class::C3::Adopt::NEXT documentation for details at ...

この問題は、連載第2回でも紹介したように、基本的にはuse NEXTの行があればuse MRO::Compatにして、$self->NEXT::foo(@_)となっている部分を、next::method(またはmaybe::next::method)に直せば解決します(逆に、それで解決しない場合はロジックを見直したほうがよいかもしれません⁠⁠。

package MyApp::Model::DB;

use strict;
use warnings;
use base 'Catalyst::Model';
use NEXT; # → use MRO::Compat;

sub new {
  my $self = shift;
  $self->NEXT::new(@_);  # → $self->next::method(@_);
}

1;

CPANモジュールについては今後順次対応が進んでいくものと思われますが、この警告、困ったことにCatalyst関係のモジュールについては開発版以外警告を出さない仕掛けが入っているので、コンポーネントのテストだけでは表面化しないおそれがあります。

# Kill Adopt::NEXT warnings if we're a non-RC version
unless (_IS_DEVELOPMENT_VERSION()) {
    Class::C3::Adopt::NEXT->unimport(qr/^Catalyst::/);
}

もちろん時期的なものを見れば問題があるモジュールかどうかはおおよそ判断がつくので、この際古いモジュールは使わないようにするのもひとつの手ですし、重要なモジュールについては順次Catalystコアの開発チームから改善の指導が行われるものと思われますが、これまでにCatalyst関連のモジュールをリリースされてきた方は、この機会にいちどコードを見直してみたほうがよいかもしれません。

Unknown error?

もうひとつ、これは本来Catalystの問題ではないのですが、Perl 5.10.0を使っている場合、コントローラ内部で(構文が間違っていたり、myなどの指定を忘れたせいで)⁠Global symbol "..." requires explicit package name」というエラーが発生すると、⁠Unknown error」という、実にわかりづらいエラーメッセージが返ってくることが知られています[1]⁠。

この問題はすでにPerl 5.10のソース上では解決されているので、5.10.1がリリースされれば直るのですが、万一このエラーに遭遇した場合は、perlの-cオプションや、Test::Moreのuse_okテストなどを利用して各ファイルの構文チェックをしてみてください。

どうしても気になる方はPerlにパッチをあてて再コンパイルするという方法もあります。また、ディストリビューションによってはPerlをアップデートすればこのパッチが適用されたバージョンが手に入ることもあります。

そのほかの問題

Catalyst 5.8に同梱されているCatalyst::Upgradingには、このほかにもいくつか、5.7系列から5.8系列に移行する際に注意したほうがよい問題が紹介されています。また、Catalyst::Deltaには5.8系列になってなにが変わったかが簡単にまとめられています。

ごくふつうのCatalystユーザであれば気にすべき問題はそれほど多くはありませんが、なにか問題にいきあたった場合はまずこれらのドキュメントにあたってみるとよいでしょう。

本当にいますぐ移行する必要はある?

Catalyst 5.8を使うことで得られるメリットについては次回以降少しずつ紹介していきますが、今後特に機能を追加する予定がなく、いまのままで安定稼働しているアプリケーションについては、あわてて移行する必要はありません。

ただし、いまも活発に開発が続いているアプリケーションについては、自分でも意識しないうちにMooseベースのコンポーネントが入り込んでくる可能性がありますので、早いうちに検証を行って、問題が出たらCatalystの開発チームにフィードバックしておいたほうがよいでしょう。

今後のロードマップについてはCatalyzed.orgのインタビュー記事で、開発チームのトマス・ドラン(Tomas Doran)氏が「次の仕事は、本当は開発版のうちにバグレポートを出してほしかった人たちから届いたバグをつぶすこと、それからドキュメントを更新して、プラグインやコンポーネントの作者たちに更新をうながすこと」とコメントしています。直してほしい問題は、更新サイクルが速いうちに報告しておくのが吉です。

おすすめ記事

記事・ニュース一覧