だるろぐ

だるいぶろぐです

TheSchwartzのサンプルも書いた

http://d.hatena.ne.jp/hirafoo/20101205/1291562525 のGearmanサンプルに続いてTheSchwartzのサンプル。
GitHub - hirafoo/queuing_sample

TheSchwartzとは

キューイングの実装の一つ。Gearmanは全てがオンメモリだけどTheSchwartzはサーバにデータベースを使用する。
Gearmanは早くて信頼性が”高いわけではなく”、TheSchwartzは遅くて信頼性が高い。

まずはDBを作る

スキーマはモジュールのソースに同梱されている。不親切なことに、そんな事はどこにも書いていない。

% sudo cpanm --look TheSchwartz
# ls doc/schema.sql

これ。

で、作る。テーブル名は自分で好きなのを使える。

% cat create_theschwartz_db.txt
mysql -u root -e 'create database theschwartz_sample'
mysql -u root theschwartz_sample < theschwartz_schema.sql

今回はmysql使ってますがソースのアーカイブの中にはPostgreSQLと(一応)SQLiteのもあるのでそっちがいい人はそっちをどうぞ。

使う

ワーカーを起動する。

% ./theschwartz_worker_runner.pl

TheSchwartzにおいてはサーバはDBMSがその役割を担う。ので裏でDBが動いているならperlでサーバを用意する必要は無い。
逆にいうとDBMSが無い環境ではTheSchwartzは使えない。まぁ、そんな環境でキューイングしようなんて思わないのでどうでもいい。

そしてこのスクリプトもやっている事は前回のGearmanのワーカーの起動スクリプトと同じく、あらかじめキューイングにさせるジョブを読み込んだプロセスをpreforkしている。
で、前回書いたとおりgearmanはジョブの登録をするとき関数単位でするが、 TheSchwartzはモジュール単位でする。
前回は MyApp::Gearman::Function に実際に実行されるジョブを書いた。このモジュールにジョブを全部つっこめるわけだ。
が、TheSchwartzは基本1モジュール1ジョブのスタイルである。なので今回の実装としては

  1. 全てのジョブのベースとなる MyApp::TheSchwartz::Function を作成
  2. 実際のジョブの内容を記述するモジュールは MyApp::TheSchwartz::Function 以下の名前空間に配置。それらは全て MyApp::TheSchwartz::Function を継承する

とした。また、 TheSchwartz では「ジョブが行われるときは work という名前の関数が呼ばれる」ってルールがあるのだけど、 各ジョブには do_work という名前の関数のみを作り、各ジョブの .pm の継承元である MyApp::TheSchwartz::Function::work の中で do_work を呼んでいる。
work の前後にフックを挟める実装である。何かのロギングとかすればいいです。

sub work {
    my ($class) = shift;
    my TheSchwartz::Job $job = shift;

    say "work begin: $class";
    $class->do_work($job->arg);
    $job->completed;
    say "work end: $class";
}

ワーカーにジョブを登録する

GearmanでもTheSchwartzでも、作られたワーカーには、そいつはどのジョブが出来るのかを覚えさせなければならない。
前回は MyApp::Gearman::Worker が MyApp::Gearman::Function に記述されているジョブを読み込んで、それら全てを登録させていた。
ここ。register_function

    my $function_class = "MyApp::Gearman::Function";
    my $functions = Class::Inspector->functions($function_class);
    for my $func (grep /^job_/, @$functions) {
        (my $funcname = $func) =~ s/^job_//;
        $self->worker->register_function($funcname => $function_class->can($func));
    }

TheSchwartzの場合は MyApp::TheSchwartz::Worker が MyApp::TheSchwartz::Function 以下の名前空間にあるモジュールを全て読み込み、そのパッケージ名を登録している。

    for my $m (__PACKAGE__->components) {
        $m->require;
        $worker->can_do($m);
    }

そこらのサンプルでは「1個のジョブを作成し、その1個だけ実行させる」のばっかりなので、ベタに

$worker->register_function(job_function_name1 => $ref_of_job1);

とか

$worker->can_do('My::Job::Function::Name');

とかしてしまう。むしろ、1個のワーカーは特定の1個のジョブしか登録できない、とか勘違いしかねない。
そんな事はないんですよと。


ジョブの登録には TheSchwartz::Simple を使うことも可能。素の TheSchwartz は内部で Data::ObjectDriver を使ってますが、Simple はDBIしか使ってないので more faster だと。
まあ TheSchwartz 使っててそこまで軽さ・早さを求めるのかというと疑問ですがケースバイケースですね。

引数を渡す

DBを通じてジョブを登録したりワーカーにジョブを振ったりする時に、TheSchwartzが自動でシリアライズとかデシリアライズをしてくれるのでユーザが明示的に何かする必要は無い。

ワーカーに仕事をさせる

% perl enqueue_theschwartz.pl -Foo
% perl enqueue_theschwartz.pl -Bar

とすれば theschwartz_result に何か書かれる。あとDBに出来たテーブルも見てみると大体何がどうなってるか分かる。
個人的に主キーの名前が id じゃないのが気に入りません。

余談

そもそも何で急にGearmanとTheSchwartzの記事書いたのかというと自分が勉強したのを書きたかっただけです。
今回書いたソースで使った俺の知識の出所の大半は「4gbpsを超えるwebサービス構築術」と上司のコードと前回書いたようにこちらです。むしろパクってます。まあ、なので、参考になると思います。
元は華麗なソースなのに俺が手を加えたせいで酷くなってますが。意図的に酷く(というか省略)した個所もある。
要は「まぁこれなら公開してもいいんじゃないかな」ってことをしてます。


省略したのは例えば

  • ワーカーにジョブが失敗したときのリトライ回数の制限を設ける
  • 失敗時の処理をもっと詰められる
    • 再度ジョブを登録するとか、sleepしてからリトライするとか
  • シグナル処理もする

とか。色々やれることありますね。


あとキューイングの説明は、俺がグダグダ書くよりこちらを見るといいです。
http://d.hatena.ne.jp/tokuhirom/20071017/1192589429