追記
CPANリリースしました
http://search.cpan.org/dist/Scope-Container/
/追記
mod_perl のアプリケーションでは、Apacheモジュールの提供するpnotesを使うとリクエスト毎のデータを簡単に持つことができます。pnotesに入れたデータはリクエストの処理が終了したところで自動的にクリーンアップされます。これを利用したのがリクエストごとにインスタンスを作成破棄できる、Apache::Singleton(::Request)です。
また、pnotesはデータベースの接続の管理にもしばしば使われます。1リクエストを裁いている間だけデータベースとの接続を維持し、リクエストが完了したところで接続を閉じるような処理に利用されています。このようにすることでmod_perlのプロセス数分(数百)の接続がMySQLに常に張られることもなく、また1度のリクエストの中で何度も接続を行うコストを押さえる事ができます。
ソースコードにすると以下のような感じ
my $dbh;
if ( $ENV{'MOD_PERL'} ) {
$dbh = Apache2::RequestRec->r->pnotes('dbh');
}
if ( !$dbh ) {
$dbh = DBI->connect($dsn,$user,$pass,\%attr);
if ( $ENV{'MOD_PERL'} ) {
Apache2::RequestRec->r->pnotes('dbh',$dbh);
}
}
return $dbh;
pnotesに入れることで、リファレンスカウントが上がるので、アプリケーションで$dbhが使われなくなっても維持され、リクエストが終わったところで初めてオブジェクトが破棄されます。
時は変わって2010年代。PSGI/PlackでWebアプリケーションを作るのが当たり前になっていますが、その際にpnotesのような機能をどのように実現したらいいのか議論になることがあります。AmonやKamui、Pickelsと言ったフレームワークではリクエスト毎に生成されるContextを持っているので、これを利用するのが良さそうです。
しかし、複雑化していく現在のWebアプリケーションではWebからのアクセスをWebアプリケーションサーバが裁くだけではなく、GearmanやQ4M、ZeroMQといったJobQueue、メッセージングシステムもバックエンドで多く使われています。このバックエンド処理でも、1回のJobの処理中にWebアプリケーションのリクエストと同じように扱えるContextがあると便利です。データベースの接続はこのContextに保管することでJob処理中1回だけ行い、処理が終われば自動的に接続を廃棄できるはずです。
このようなContext機構を、WebアプリケーションではフレームワークのContext、Job
Workerでは別のContextとならずに、1つの方法で実現できないか、さらにこの考え方を押し進めて、リクエストやJobだけに限らない任意のタイミング(スコープ)でContextの生成・破棄を行えないかということで書いてみたのが、Scope::Container
です
githubにあげてあります: http://github.com/kazeburo/Scope-Container
(Scope::Contextなのかなと思う事あり)
実はこのような思想の元、書かれたモジュールがCPANには一つあります。mixiの広木さんの書いたScope::Sessionです。しかし、使い方がぱっとみでよくわかりませんでしたのでぜひ、解説記事求むです。
追記
解説記事書いて頂きました
Sessionの生成・破棄を任意のタイミングで制御可能にするScope::Session
/追記
Scope::Container を use すると2つのメソッドをexportします。基本的な使い方は以下のような感じ。
use Scope::Container;
sub foo {
my $key = shift;
scope_container($key);
}
{
my $guard = start_scope_container();
#データ保存
scope_container('key',value);
#データ取得
my $value = scope_container('key');
my $value2 = foo('key') #上と同じ結果
#$guardがスコープから外れるので自動的に、保存したものは削除される
}
start_scope_container は
Guardオブジェクトを返します。そしてこのオブジェクトが破棄されるまでを1つのスコープとして扱います。この間にデータの出し入れを自由に行え、$guardが破棄されるタイミングで全てのデータがクリアされます。
追記
gfxのpatchで最新版ではGuardに依存しなくなりました。
Scope::Containerオブジェクトを返します
/追記
そして以下がデータベースへの接続を管理するサンプル
sub connect {
my $class = shift;
my @dsn = @_;
my $dbh;
my $dsn_key = Data::MessagePack->pack(\@dsn);
my $connector = scope_container($dsn_key);
eval {
$dbh = $connector->_dbh;
};
return $dbh if $dbh;
my $connector = DBIx::Connector->new(@dsn);
$dbh = $connector->dbh;
scope_container($dsn_key, $connector);
return wantarray ? ( $dbh, $connector ) : $dbh;
}
dsn、ユーザ名、パスワード、オプションをMessagePackでシリアライズしたものをキーとしてScope::ContainerにDBIx::Connectorのオブジェクトを保持させます。複数スレーブ対応版のサンプルも書いてみたのでgistにあげてあります。
Scope::ContainerをPlackで使う場合は、Middlewareで
sub call {
my ($self, $env) = @_;
my $guard = start_scope_container();
try {
$self->app->($env);
} catch {
die $_;
} finally {
undef $guard;
};
}
としておけばアプリケーションの中でscope_contaierが利用でき、GearmanのWorkerなら
my $guard;
$worker->work(
on_start => sub {
$guard = start_scope_container();
},
on_complete => sub {
undef $guard;
},
stop_if => sub {
...
}
);
としておけばWebアプリケーションと同じように動いてくれると思われます。
Scope::Containerはlocal $FOOBARで代用ができるところも多いと思いますが、汎用で使えるものとしてどう思いますでしょうか?