時計を壊せ

駆け出してからそこそこ経ったWebプログラマーの雑記

正月発火村に行って来た。あるいはXOClockの話。

さりげなくないけど今年初bloggingです。
あけましておめでとうございます。

正月発火村とは

主に 僕 と @kfly8 が「ハッカソンやりたい!」とか言って適当に集まった面子で、
水上温泉に行ってやってきたhackathonです。
僕はXOClockというものをPerlで開発していました。

XOClockってなに

指定した時刻に指定したJobを実行するJobQueueサーバーです。
例えば、

Facebookでつながってるフレンドをあらかじめ選択して登録しておくと、
2012-02-14 00:00:00にそのフレンドのWallに「Give me chocolate!!」というpostをするWebサービス

を実装したいケースなどで役に立つと思います。

ぶっちゃけ、あまり需要は無いと思いますが、
こんなものがあったら便利になるケースもありそうかなーって思って作りました。

バレンタイン支援WebサービスのWorkerを実装してみる。

上の例を実際に実装してみましょう。*1

まずは以下のような、
アクセストークンとpost対象のユーザーのID一覧を貰えば、
「Give me chocolate!!」
とpostする簡単なWorkerを書きましょう。

package MyProj::Worker::Valentine;
use strict;
use warnings;
use utf8;
use 5.10.0;

use parent qw/XOClock::Worker/;

use Facebook::Graph;

sub run {
    my($self, $args) = @_;

    my $fb = Facebook::Graph->new(access_token => $args->{access_token});                                                                 
    foreach my $id (@{ $args->{id_list} }) {
        $fb->add_post
            ->to($id)
            ->set_message('Give me chocolate!!')->publish;
    }
}

1;


次に、簡単なconfigを書きましょう。
これはWorkerを登録するために必要です。
このファイルをxoclock.yamlという名前で保存してみましょう。

max_workers: 50
worker:
  Valentine: MyProj::Worker::Valentine

これをconfig_fileで指定してserverを起動しましょう。

$ xoclockd --config_file=xoclock.yaml
2012-01-10T01:07:44 [INFO][21229] running on pid: 21229. at /Users/karupanerura/perl5/perlbrew/perls/perl-5.14.2/lib/site_perl/5.14.2/XOClock.pm line 119
2012-01-10T01:07:44 [INFO][21229] load config from 'xoclock.yaml'. at /Users/karupanerura/perl5/perlbrew/perls/perl-5.14.2/lib/site_perl/5.14.2/XOClock.pm line 81
2012-01-10T01:07:44 [INFO][21229] create JSONRPC Server. listen: 0.0.0.0:5312 at /Users/karupanerura/perl5/perlbrew/perls/perl-5.14.2/lib/site_perl/5.14.2/XOClock/Server.pm line 301
2012-01-10T01:07:44 [INFO][21229] server start. at /Users/karupanerura/perl5/perlbrew/perls/perl-5.14.2/lib/site_perl/5.14.2/XOClock.pm line 182

みたいな感じで起動出来ると思います。

あとはWebApp等から、

use XOClock::Client;
my $client = XOClock::Client->new(
    host => '127.0.0.1',
    port => 5312,
);
my $res = $client->enqueue(
    name      => 'Valentine',
    datetime  => '2012-02-14 00:00:00',
    time_zone => $time_zone, # 2012-01-10 08:00頃追記。default: 'GMT', example: 'JST'
    args      => +{
        access_token => $access_token,
        id_list      => \@id_list,
    },
)->recv;

とJobをenqueueしてあげると指定したタイムゾーン2012-02-14 00:00:00にJobが実行されて、
ユーザーが指定したフレンドにGive me chocolate!!と勝手にpostされると思います。


これを普通のJobQueueで実装しようとするとちょっと骨が折れると思いますが、
XOClockを使えばこのように簡単に実装出来ます。

どんなふうに動いてるのか

主にAnyEvent上で動いています。
ただし、Workerは

  • 同じ時刻や、近い時刻でJobが複数発生する事がある。
  • Workerではブロックせざるを得ない処理を行う事がある。
  • Workerでやらざるを得ない処理は単純に処理量が多い場合が多い。
  • 意図しないエラーが発生した場合にそれを捕捉してログに残したい。

等の理由で別プロセスで動くようになっています。
終了コードが0以外の時はエラーが発生したとして、リトライしたり、Jobが失敗した事をログに残したりします。


あとは、
enqueue等を受ける所はAnyEvent::JSONRPC::Liteで、
signalをcatchする所はAnyEvent->signal*2
子プロセスの終了を捕捉する所はAnyEvent->childでやっています。


Forkまわりは最初はParallel::ForkManagerを使ってやっていたのですが、
waitする度にblockingしてしまうので、いろいろとhackをする事になってしまいました。
結局waitでblockしない(AnyEvent->childで子プロセスの終了を捕捉する)ようにしたAnyEvent::ForkManagerというモジュールを書きました。

AnyEvent::ForkManager

Parallel::ForkManagerと同じ事をAnyEventのイベントループをblockingしないように簡単に出来るようにしたモジュールです。
これは別途記事を書きます。

今後の展望

以下のような事がしたいなー、ぼんやりと考えています。
Githubで公開しています。

  • 監視用APIを用意する。
    • 動いているWorkerの数(どのJobがいくつ動いているのか)
    • Worker起動予約Queueの数
    • 時間予約Queueの数
  • cronの様にも使えるように繰り返し実行する機能を実装する。(1時間毎とか)
  • Dainamoに対応させる。
  • fork周りを外出ししたAnyEvent::ForkManagerを使うようにする。

正月発火村感想

  • 人が開発してるモノ見てて面白かった。
  • 温泉すばらしい。
  • 料理おいしい。
  • 雪見ながら開発出来た!
  • 環境が普段と違うので集中出来た!
  • まとまった長い時間を使える。
  • でも飯作らなきゃとか考えなくて良い。

温泉行ってまでコード書きたくないという人も居ると思いますが、
温泉旅行って結構、温泉+αな旅行になると思うので、

  1. αでhackathonやってると思えばあまり損な気分にもならない筈。

ぼくは雪を見ると無条件でテンションが上がる人なので、だいぶ楽しいhackathonでした!


他の人がやってたことについては、
なんか凄い事してる!!!
って感じでなんか凄すぎてなんで動いてるのかあまりよくわかってなかったので、
あとでソースじっくり読んで勉強したいと思います!
でも、かなり刺激を受ける事が出来てモチベーションがだいぶ上がりました!
ありがとうございました!

ngircdを立ち上げたりもしたけど、
基本的にはTwitterでやりとりしたりしてたので、
Togetterでまとめてみました。
気になる方はどうぞ!

*1:ここではWorker以外は実装しません

*2:SIGINT,SIGTERM,SIGQUITでgraceful shutdown, SIGHUPでgraceful restartします