Linuxなどのメモ書き

mod_perlハンドラを書く


Rev.5を表示中。最新版はこちら

このテキストは

mod_perl2.0ドキュメントの"Writing mod_perl Handlers and Scripts"(http://perl.apache.org/docs/2.0/user/coding/coding.html)の和訳です。

現在編集中

mod_perlハンドラとスクリプトを書く

目次




# Perl Specifics in the mod_perl Environment

    * BEGIN Blocks
    * CHECK and INIT Blocks
    * END Blocks
    * Request-localized Globals
    * exit

# ModPerl::Registry Handlers Family

    * A Look Behind the Scenes
    * Getting the $r Object

# Threads Coding Issues Under mod_perl

    * Thread-environment Issues
    * Deploying Threads
    * Shared Variables

# Maintainers
# Authors

説明

この章ではmod_perlのコーディングの詳細、通常のPerlコーディングとの違いを扱います。その他のほとんどのPerlコーディングに関する情報はperlのmanページや書籍で取り扱われています。

前提

原文なし

Where the Methods Live

mod_perl 2.0では多くのモジュールに跨ってメソッドが存在します。これらのメソッドを使うためにメソッドを含むモジュールは最初にロードしておかないといけません。そうでないと、mod_perlは「メソッドを見つけられない」とエラーにします。どのモジュールが必要かを探すにはModPerl::MethodLookup モジュールが使えます。

Techniques

メソッドハンドラ

関数ハンドラに加えてメソッドハンドラを使うことができます。メソッドハンドラは継承のメリットを生かしたコードを書きたい場合に便利です。mod_perl2のメソッドとしてハンドラを作る場合、method属性を使ってください。

属性の詳細についてはPerlのmanページを参照してください(perldoc attributes)。

例:

  package Bird::Eagle;
  @ISA = qw(Bird);

  sub handler : method {
      my ($class_or_object, $r) = @_;
      ...;
  }

  sub new { bless {}, __PACKAGE__ }

そして、これを以下のようにして登録します。

  PerlResponseHandler Bird::Eagle

mod_perlはハンドラがmethod属性を持っている場合は、2つの引数をハンドラに渡します。上記のコードにもあるようにオブジェクトもしくはクラス名(どのように呼び出されたかに依存します)とRequestオブジェクトです。

以下のようにClass->methodのような形式が使われた場合は、

  PerlResponseHandler Bird::Eagle->handler;

method属性は必要ありません。上の設定例ではhandler()メソッドはクラス(静的)メソッドとして呼び出されます。

メソッドを呼び出すのにオブジェクトを使うこともできます。例えば、

  <Perl>
      use Bird::Eagle;
      $Bird::Global::object = Bird::Eagle->new();
  </Perl>
  ...
  PerlResponseHandler $Bird::Global::object->handler

この例では、hanlder()メソッドはグローバルオブジェクト$Bird::Global::objectのインスタンスメソッドとして呼び出されます。

クリーンアップ

さまざまなフェーズの終わりで発生するクリーンアップ処理をアレンジできます。これを行うのにENDブロックは使えません。なぜなら、ENDブロックはインタプリタが終了するまで実行されないからです(Registryハンドラは例外です)。

モジュール作成者は各HTTPリクエストの後にクリーンアップ処理が必要な場合は、PerlCleanupHandlerを使うべきです。

その他のタイミングでクリーンアップ処理を行う必要がある場合は、プールオブジェクトのcleanup_registerメソッドでクリーンアップコールバックを登録することができます。使用例をいくつか示します。

サーバーのシャットダウンやリスタート時に何か行いたい場合は、startup.plでserver_shutdown_cleanup_register()で登録したクリーンアップハンドラを使ってください。

  #PerlPostConfigRequire startup.pl
  use Apache2::ServerUtil ();
  use APR::Pool ();

  warn "parent pid is $$\n";
  Apache2::ServerUtil::server_shutdown_cleanup_register((\&cleanup);
  sub cleanup { warn "server cleanup in $$\n" }

この方法は、サーバーが停止したり、再起動する時に行うべきサーバー全体のクリーンアップ処理を行うのに便利です。

接続フェーズの終わりにクリーンアップを行う場合は、コネクションプールオブジェクトにクリーンアップコールバックを割り当てます。

  use Apache2::Connection ();
  use APR::Pool ();

  my $pool = $c->pool;
  $pool->cleanup_register(\&my_cleanup);
  sub my_cleanup { ... }

あなた自身のプールオブジェクトを作成してコールバックを登録することもできます。これはオブジェクトが破壊された時に呼び出されます。

  use APR::Pool ();

  {
      my @args = 1..3;
      my $pool = APR::Pool->new;
      $pool->cleanup_register(\&cleanup, \@args);
  }

  sub cleanup {
      my @args = @{ +shift };
      warn "cleanup was called with args: @args";
  }

この例ではクリーンアップコールバックは$poolがスコープの外に出て破壊された時に呼び出されます。これはオブジェクト指向のDESTROYメソッドに非常に似ています。

Goodies Toolkit

環境変数

mod_perlは次の環境変数を設定します。

  • $ENV{MOD_PERL} - mod_perlのバージョンが設定されます

例:

  mod_perl/2.000002

$ENV{MOD_PERL}が存在しなければ、たぶんmod_perl配下で動作していません。

  die "I refuse to work without mod_perl!" unless exists $ENV{MOD_PERL};

どのバージョンが使われているのかチェックするには、次のテクニックを使うとよいです。

  use mod_perl;
  use constant MP2 => ( exists $ENV{MOD_PERL_API_VERSION} and 
                        $ENV{MOD_PERL_API_VERSION} >= 2 ); 

  # die "I want mod_perl 2.0!" unless MP2;

mod_perlは次の環境変数をエクスポートします(それらが設定されていれば)。

  • PATH - 実行可能ファイルのパス
  • TZ - タイムゾーン

これらの環境変数は%ENVでアクセスできます。

スレッド化MPMか否か

実行環境がスレッド化されたMPMかそうでないかでコードが異る動作をする必要があるならApache2::MPM->is_threadedクラスメソッドが使えます。

例:

  use Apache2::MPM ();
  if (Apache2::MPM->is_threaded) {
      require APR::OS;
      my $tid = APR::OS::current_thread_id();
      print "current thread id: $tid (pid: $$)";
  }
  else {
      print "current process id: $$";
  }

このコードはスレッド化されたMPM配下ではカレントスレッドIDを表示します。そうでなければプロセスIDを表示します。

MPM固有のコードを書く

あなたがCPANモジュールを書くなら、全てのMPMで動作しないコードを書くのはまずい考えです。開発者は全てのMPMで動作するコードを書くよう努力すべきです。異るMPM環境で異ることを行うのはまったく立派なことです。

あなたがCPANモジュールを開発しないなら、特定のMPMで動作すればあなたのプロジェクトにとって十分でしょう。

  use Apache2::MPM ();
  my $mpm = lc Apache2::MPM->show;
  if ($mpm eq 'prefork') {
      # prefork-specific code
  }
  elsif ($mpm eq 'worker') {
      # worker-specific code
  }
  elsif ($mpm eq 'winnt') {
      # winnt-specific code
  }
  else {
      # others...
  }

Code Developing Nuances

修正されたモジュールの自動リロード

META: need to port Apache2::Reload notes from the guide here. but the gist is:

  PerlModule Apache2::Reload
  PerlInitHandler Apache2::Reload
  #PerlPreConnectionHandler Apache2::Reload
  PerlSetVar ReloadAll Off
  PerlSetVar ReloadModules "ModPerl::* Apache2::*"

以下のハンドラを使ってください。

  PerlInitHandler Apache2::Reload
HTTPプロトコルハンドラをデバッグしたいなら、以下のようにしてください。
  PerlPreConnectionHandler Apache2::Reload

我々が自分達のモジュールを以下のような形式にする場合の注意です。

  package Apache2::Whatever;

  use strict;
  use warnings FATAL => 'all';

FATAL => 'all' は全ての警告をfatalエラーとして上げます。Apache2::Whateverが変更されてApache2::Reloadによって再読み込みされた時、リクエストは中断されてしまいます。ですから、もしあなたがこの非常に健全な方法にしたがってApache2::Reloadを使いたいなら以下のようにエラーチェックの厳密さを変更してください。

  use warnings FATAL => 'all';
  no warnings 'redefine';

redifne 警告を取得したいが、non-fatalにレベルを落とす場合は次のようにしてできます。

  use warnings FATAL => 'all';
  no warnings 'redefine';
  use warnings 'redefine';

Perl 5.8.0なら1行で全て行えます。

  use warnings FATAL => 'all', NONFATAL => 'redefine';

もしあなたのコードが古いバージョンのPerlで使われるかもしれないのなら、この新しいは使わないほうがよいでしょう。

さらなる情報はperllexwarn manページを参照してください。

Integration with Apache Issues

以下ではmod_perl開発者に関係するApacheの振る舞いの詳細について議論していきます。

HTTPレスポンスヘッダ

HTTPレスポンスヘッダの生成

HTTPレスポンスヘッダを生成する最もよい方法はmod_perl APIを使うことです。いくつかの共通のヘッダには専用のメソッドがあります。その他のものも、headers_outテーブルを直接操作することで設定できます。

例えば、Content-typeヘッダを設定する場合は、$r->content_typeを呼び出すべきです。

  use Apache2::RequestRec ();
  $r->content_type('text/html');

カスタムヘッダMy-Headerを設定する場合は、以下のようにしてください。

  use Apache2::RequestRec ();
  use APR::Table;
  $r->headers_out->set(My-Header => "SomeValue");

もし、registryスクリプトの中であれば、Apache2::RequestRecオブジェクトにまだアクセスできます。

レスポンスヘッダを出力することによってヘッダを作成するもっと遅い方法を採ることもできます。この方法はPerlOptions +ParseHeadersが有効な場合のみ動作します。

   print "Content-type: text/html\n";
   print "My-Header: SomeValue\n";
   print "\n";

この方法はApacheがヘッダを識別するためテキストをパースする必要があるので遅くなります。
またいくつかの制限があります(これから議論します)。

このアプローチでは、STDOUTファイルハンドルがprint後にデータをフラッシュしないようになっていることを確認しなければなりません(これはPerlの特殊変数$|に設定されています)。STDOUTが現在select()されており$|がそれに影響するものと仮定しています。

例えばこのコードは動作しません。

   local $| = 1;
   print "Content-type: text/html\n";
   print "My-Header: SomeValue\n";
   print "\n";

$|が真だと最初のprint()呼び出し後、すぐにデータがフラッシュされます。データは内部のHTTPヘッダパーサーに送られます。そしてヘッダが"\n\n"で終わっていないように見えるのでエラーになります。解決方法としてはSTDOUTをすぐにフラッシュさせないようにする方法があります。

   local $| = 0;
   print "Content-type: text/html\n";
   print "My-Header: SomeValue\n";
   print "\n";

変更をlocal()化していることに気をつけてください。これだと、他のコードに影響しません。

もし、あなたが1行づつヘッダを送信しそれらの合計長が8kを越えると、ヘッダパーサーの問題にまたぶつかるでしょう。mod_perlは8kバッファがいっぱいになるとデータをフラッシュするからです。この場合の解決法はヘッダを1行づつ出力しないようにすることです。変数にヘッダを格納して全体を一気に出力します。

mod_cgiを使う場合はこれらの問題は関係ありません。Perlによるフラッシュは無視されるからです。mod_cgiは外部プロセスへのパイプをオープンしプロセスから送られてきた出力を読み取るだけです。

この節の始めで説明したように、$rを使ってヘッダを設定するなら、これらの問題に遭遇することはありません。

最後にApacheにヘッダを送らせず、あなたが自分でヘッダ(non-parsed headers handlers)を送信したい場合は、$r->assbackwards メソッドを使ってください。スクリプト名がnph-で始まる場合は、registryハンドラはこのように動作することに注意してください。

HTTPレスポンスヘッダの強制出力

Apache 2.0はHTTPレスポンスヘッダの送信を強制するメソッドを提供しません(Apache 1.3ではsend_http_header()が相当)。HTTPレスポンスヘッダはレスポンスBodyの最初のビットを見つけるとヘッダを作成するCore outputフィルタによりすぐに送信されます。レスポンスハンドラがボディデータの最初の部分を送信するときに、mod_perlの内部バッファやいくつかのoutputフィルタによりキャッシュされたりするかもしれません。レスポンスハンドラはレスポンスの送信にかかわっている全コンポーネントにデータ送信を伝えるために出力をフラッシュする必要があります。

例えば、もしハンドラが比較的時間のかかる処理(遅いDBの検索とか)を行う必要があり、すぐにクライアントが何も受信しないとタイムアウトするかもしれない場合、ハンドラの始めにContent-Typeヘッダを設定してすぐにフラッシュしたいかもしれません。

  sub handler {
      my $r = shift;
      $r->content_type('text/html');
      $r->rflush; # send the headers out

      $r->print(long_operation());
      return Apache2::Const::OK;
  }

これが動作しないなら、サードパーティ製のoutputフィルタが設定されていないか確認してください。Improperly written filter はデータをフラッシュするのにコマンドを無視するかもしれません。

HTTPレスポンスBodyの送信

mod_perl 2.0ではレスポンスBodyはレスポンスフェーズの間のみ送ることができます。もっと前のフェーズで送信しようとすると、error_logファイルにログが出力されて失敗します。

これは、Apache 2.0のHTTPアーキテクチャによるものです。レスポンスフェーズの前ではHTTPレスポンスフィルタ設定されていないのが問題の1つです。

シグナルハンドラの使用

サードパーティのApache 2 モジュールはシグナルに頼ったコードを使うのを避けるべきです。
これは、シグナルの使用は典型的にスレッドセーフでないのと、シグナルに頼ったモジュールはポータビリティがないかもしれません。あるシグナルはスレッド化されていないMPMでしか動作しないかもしれません。例えばalarm()はprefork MPMで使うことができますが、他のMPM環境化では動作しないでしょう。さらにApache開発者は現在たまたま動作しているシグナルが将来のApacheリリースでも動作することは保証しません。だから、あなた自身の責任で使ってください。

スレッドでも動作するようにシグナルを使ったコードを修正できるとよいです。例えば、もし長時間動作する可能性のあるI/Oをトラップするのにalarm()を使っていたならselect/pollを使うようにI/Oロジックを修正することができます。もしくはAPR I/Oを使っているならAPRパイプ/ソケットにタイムアウトを設定できます。例えばUnix上のApache 1.3はブロッキングI/O呼び出しを行い、それをタイムアウトで中断するSIGALRMを送信するのを親プロセスに頼っていました。Apache 2.0ではI/O処理のタイムアウトをサポートしているAPRが使われます。だから、シグナルや他のスレッドセーフでない手法は必要ありません。

CPUタイムアウトのハンドリングはもう1つの例です。インターバルでタイムアウトを明示的にチェックするようにロジックを修正することで実現できます。

prefork MPM下でのalarm()について説明します。POSIXシグナルは動作するように見えます。しかし、Perl 5.8.xが必要です。例えば、

  use POSIX qw(SIGALRM);
  my $mask      = POSIX::SigSet->new( SIGALRM );
  my $action    = POSIX::SigAction->new(sub { die "alarm" }, $mask);
  my $oldaction = POSIX::SigAction->new();
  POSIX::sigaction(SIGALRM, $action, $oldaction );
  eval {
      alarm 2;
      sleep 10 # some real code should be here
      alarm 0;
  };
  POSIX::sigaction(SIGALRM, $oldaction); # restore original
  warn "got alarm" if $@ and $@ =~ /alarm/;
詳細はこちらを参照:
http://search.cpan.org/dist/perl/ext/POSIX/POSIX.pod#POSIX::SigAction

5.6.xでは$SIG{ALRM}を使うことができました。しかしDSO modperl環境でしか動作しません。さらに5.8.0からPerlはシグナルを安全に処理するためにシグナルの発生を遅延させます。この変更は以前動作していたコードをだめにするかもしれません。詳細は以下を参照。

http://search.cpan.org/dist/perl/pod/perl58delta.pod#Safe_Signals
http://search.cpan.org/dist/perl/pod/perlipc.pod#Deferred_Signals_%28Safe_Signals%29

例えば、以下のalarmコードの場合、

  eval {
      local $SIG{ALRM} = sub { die "alarm" };
      alarm 3;
      sleep 10; # in reality some real code should be here
      alarm 0;
  };
  die "the operation was aborted" if $@ and $@ =~ /alarm/;

これは、今はもう動かないかもしれません。5.8.1以降ならプログラム実行開始後すぐに(mod_perlならstartup.pl内)、以下のように設定してシグナルの安全性をすり抜けることができます。

  $ENV{PERL_SIGNALS} = "unsafe";

この記事を書いている時点では、この次善策はMacOSXではうまくいきません。代わりにPOSIXシグナルを使わなければいけません。

詳細な情報は以下を参照してください。

http://search.cpan.org/dist/perl/pod/perl581delta.pod#Unsafe_signals_again_available
http://search.cpan.org/dist/perl/pod/perlrun.pod#PERL_SIGNALS

あなたが5.8.xを使っているのであっても、この節で説明したPOSIX APIを使う方が望ましいです。

Perl Specifics in the mod_perl Environment

以下の節ではmod_perl環境でのPerlの振る舞いについて議論します。

BEGINブロック

Perlはコードをコンパイルするときに、できるだけ早くBEGINブロックを実行します。mod_perl環境でもこれは同じです。しかし、mod_perlは通常スクリプトとモジュールを一回しかコンパイルしないので、親サーバー(サーバー動作開始時)か各子プロセス(モジュールを使用する最初のリクエスト時)のどちらかでBEGINブロックは一回だけ実行されます。perlmodのmanページではBEGINブロックは一度だけ実行されて、実行されればすぐに未定義になると説明しています。これは、mod_perl環境では(例えばまだロードされていなかったなどの理由で)コードのコンパイルがたまたま発生しなければ、受信したリクエストに対してレスポンスしている間にBEGINブロックが実行されることがないことを意味しています。

require()やuse()で読み込んだモジュールやファイル内のBEGINブロックは、以下のように実行されます。

  • サーバ動作開始時に親プロセスによって読み込まれたら、一回だけ実行
  • 親プロセスによって読み込まれていなければ、各子プロセスやPerlインタプリタ毎に一回実行
  • Apache2::Reloadでモジュールが再読み込みされる場合は、各子プロセスやPerlインタプリタ毎に一回実行
  • %INCを弄った場合は、予測不能

BEGINブロックの振る舞いはModPerl::RegistryとModPerl::PerlRunとこれらのサブクラスでは異ります。

CHECK,INITブロック

CHECKとINITブロックはソースコードのコンパイルが完了して、プログラムが動作する前に実行されます。


最終更新 2007/05/25 18:41:15 - kztomita
(2007/05/25 17:29:38 作成)


リンク

その他のWiki
Linuxメモ
Xnuメモ

会社
(有)ビットハイブ
受託開発やってます。

よくやる仕事

・Webシステム開発(LAMP環境)
・Linuxサーバー設定関連
サーバー移転作業代行

開発事例にデジタルカタログ/マンガビューワーを追加しました。

draggable.jsのスマホ対応版デモページを追加しました。説明はこちら

検索

Adsense
最近のコメント