perlのGearmanとPHPのNet_Gearmanでreblogサーバを作る

ケータイからtumblrのdashboardに入ってreblogするためのreblog.ido.nuで使うためにPHPからperlのGearmanみたいなのを使いたくてtwitterでぼやいてみたらTwitter / ippei ogiwara: @ku ぐぐったらでてきた http://tinyur…というのを教えてもらったのでGearmanでreblogサーバを作ってPHPから呼んでみました。

Twitter / ippei ogiwara: @ku おもしろそうだから、使ってみたらおしえてください
というわけで簡単にレポート。

Gearmanのインストール

まずGearmanが入っていなかったのでlog4ZIGOROu : Gearmanを使ってみたを参考にインストール。

サーバはFreeBSDです。Sys::Syscallだけforceで入れました。

sudo cpan -i Sys::Syscall
sudo cpan -i Danga::Socket
sudo cpan -i Gearman::Client
sudo cpan -i Gearman::Server
sudo gearmand --daemon --pidfile=/var/log/gearmand.pid --debug=1

で問題なく起動しました。

reblogサーバを書く

reblogするやつを書きたかったんだけど、時間がかかるので非同期に実行したくて、そこでGearmanが使いたかったというかWWW::Mechanizeが使いたかったのでperlで動くGearmanが使いたかったのでした。Mechanizeでてきとうにreblogするやつを実装。
log4ZIGOROu : Gearmanを使ってみたではタスクに渡す引数をfreeze/thawでやりとりしていますが、今回はPHPから呼び出すのでfreeze/thawが使えないのでJSONでパラメータを渡します(はじめ気がつかなくてちょっとはまった)。

呼び出す側ではemailとパスワードとreblogするpostのidを引数で渡します。

#!/usr/bin/perl
use strict;
use warnings;
use Gearman::Worker;
use JSON;
use WWW::Mechanize;
my $mech = WWW::Mechanize->new();
my $worker = Gearman::Worker->new;
$worker->job_servers(qw|localhost|);
$worker->register_function(
    reblog => sub {
        my $job = shift;
        my @args = @{ from_json( $job->arg ) };
                sleep 3;
        return reblog(@args);
    }
);
$worker->work while 1;
sub reblog {
        my ($email, $passwd, $postid) = @_;
        $mech->get( 'http://www.tumblr.com/login' );
        sleep 3;
        $mech->submit_form(
                        fields      => {
                                email    => $email,
                                password    => $passwd
                        }
        );
        my $headers = $mech->response->headers;
        if ( $headers and $headers->{'refresh'} ne '0;url=/dashboard' ) {
                warn "header ng. $email $passwd ";
                return 1;
        }

        sleep 3;
        $mech->get( "http://www.tumblr.com/reblog/$postid" );
        sleep 3;
        $mech->submit_form;

        0;
}

Net_Gearmanのインストール

pearでいっぱつでインストールできます(あたりまえ?)。

sudo pear upgrade   http://bugs.joestump.net/code/Net_Gearman/Net_Gearman-0.0.4.tgz

どう使うのかあんまり書いてなかったのでちょっとつまづきましたがTasksをまねして書いたらいけました。

注意点をいくつか。

  • Net_Gearman_Taskのコンストラクタの4番目の引数は、デフォルトでNet_Gearman_Task::NORMALになっていて、これだとNet_Gearman_Client::runSetが呼ばれると、Gearmanにタスクが登録されてGearmanでタスクの実行が完了するまで、この関数でブロックします。ページはすぐにサーブしたいから時間がかかる処理(今回はreblogですけど)を非同期で実行しようとしてるのに、ここでブロックするとぜんぜん意味がないので非同期で実行してほしいときはNet_Gearman_Task::JOB_BACKGROUNDを設定するとブロックしなくなります。
  • 同様にNet_Gearman_Task::attachCallbackを使うのもNGです。これを呼び出して処理が完了したときに呼び出される関数を設定すると、やっぱりGearman上でタスクが終了するまでブロックするようになるのでやっぱりぜんぜん意味がなくなります。Gearman上でreblogが終わるまでページがサーブされません。結果としてPHP側からはうまくいったかどうかの確認はできなくなるので、そのへんはperlでDBに書いておいてそれをPHPから参照するとかする必要アリ。
  • Net_Gearman_Taskの2つめの引数は、Net_Gearman_Taskの中でJSONでエンコードされてGearmanに渡されます。なのでGearmanのタスク側で引数がJSONになってくるものとしてコードを書く必要があります。
<?php
require_once 'Net/Gearman/Client.php';
function result($resp) {
        $t = func_get_args();
        print "response: ";
    print_r($t);
}
$set = new Net_Gearman_Set();
$email = 'ku@example.com';
$passwd = '*************';

$pid = getmypid();
$posts = array ( 24160934, 24060934, 240934);
foreach ($posts as $k => post) {
    $task = new Net_Gearman_Task(
                'reblog', array( $email, $passwd, $post),
                "$pid-$k",  Net_Gearman_Task::JOB_BACKGROUND
        );
 //   $task->attachCallback('result');
        $set->addTask($task);
}
$client = new Net_Gearman_Client(array('localhost'));
$client->runSet($set);
?>

これはPHP側のNet_Gearmanの問題かperl側のGearmanのコードがよくないのか自分の環境がへんなだけかわかりませんが、Net_Gearman_Setに複数のNet_Gearman_Taskを登録しても、なぜか一番はじめのやつしかキューに入らないようで、二番目以降のやつは実行してもらえませんでした。ちゃんと検証してはないので何が悪いのかはまだ不明。

というわけでNet_Gearmanを使うとPHPからperlのGearmanが呼べてCPANのモジュール使えて便利でした。この組み合わせは病み付きになりそう。

追記 2008/1/20

new Net_Gearman_Taskの第3引数はリファレンスにはuniqって書いてあるだけでなんなのか意味不明だったのでてきとうに文字列渡してたのですが、ここはタスクごとにユニークな文字列を渡す必要があります。このuniqが他のタスクと同じだとGearmanのサーバ側に同じidのタスクがキューに入っているとあとから追加したほうのタスクは無視されて実行されません。ので注意。nullにしたらてきとうにやってくれそうなかんじもしますが残念ながらキューにnullで入れたやつが入っているとキューイングされません。perlのGearmanのインターフェイスにはuniqみたいなのないけど隠蔽されてるのかな。

あとどうでもいいけどperlの場合も$client->do_taskだとタスクが終了するまでブロックするので、ブロックしてほしくないときはかわりに$client->dispatch_backgroundを使います。

WWW::Mechanize問題について追記 2008/1/20

日本語とか中国語が含まれているものをreblogしようとすると 500 wide character in syswrite て出てきてsubmit_formがエラーになるのはけっきょく自分でencodeしてあげたらうまく行った。このへんいつまでたってもよくわかんない。

    my $f = $mech->current_form;

    my @forms = $mech->forms;
    foreach (@forms) {
        foreach ( @{$_->{inputs}} ) {
            my $v = $_->{value};
            $v or next;

            decode('UTF-8', $v);
            $_->{value} = $v;
        }
    }

でもこれ実行時間のほとんどはレスポンス待ちに費やされてるので処理するほうを並列化したい。Gearman側の設定いじったらできるかな。


About this entry