$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

手元開発環境でサーバを起動時のみcronのようにスクリプトを実行する(Perlの場合)

これまでPerlを利用した手元開発環境でどのようにcronを動かすか迷ってきたのだけど、その解決策が見つかったのでメモ。

課題

  • 開発サーバや本番サーバではcronで定期的にスクリプトが実行されている
  • 定期的に実行されているスクリプトが動かないと、正しく動かない機能がある
    • 例えば予約投稿みたいな機能など
  • しかし手元開発環境ではcronのように定期的にスクリプトを実行していなかった
  • 結果として、手元開発環境で手動でスクリプトを動かさないと確認できない機能があった

解決策として手元でもcrontabを書く方法もあるのだけど、この場合開発していない時も勝手に実行されるので避けたかった。

解決方法

実はProclet というツールに、サーバを起動しながら定期的に指定したコードを実行してくれる機能があるということに気づいた。詳しくはSYNOPSISを参照。

これを使ってcronに指定しているスクリプトを適当に実行するようにしてみる。

script/server.pl

use Plack::Loader;
use Proclet;

my $proclet = Proclet->new(
    color => 1
);

# サーバー起動
$proclet->service(
    code => sub {
        my $loader = Plack::Loader->load(
            'Starlet',
            port        => 3000,
            max_workers => 4,
        );
        my $app = sub {
            return [
                200,
                [ 'Content-Type' => 'text/plain' ],
                [ "200 OK" ]
            ];
        };
        $loader->run($app);
    },
    tag => 'web'
);

# cronスクリプトを登録
$proclet->service(
    code => sub {
        system "perl script/cron/hoge.pl";
    },
    tag => 'cron.hoge',
    every => '*/1 * * * *', # サーバ起動時だけ1分に1回実行
);
$proclet->service(
    code => sub {
        system "perl script/cron/fuga.pl";
    },
    tag => 'cron.fuga',
    every => '*/5 * * * *', # サーバ起動時だけ5分に1回実行
);

$proclet->run;

後はこのスクリプトを起動して、手元開発をすれば、サーバを起動しながら1分に1回script/cron/hoge.plが、5分に1回script/cron/fuga.plが実行されるようになる。最高。

Test::Time::AtというCPANモジュールをリリースしました

社内でテスト時の時間操作を便利にするTest::Time::At というモジュールがあったので、それをCPAN化してリリースしました。

テスト中の時間を止めて、sleepなどの操作をうまくハンドリングしてくれるモジュールにはTest::Time というモジュールがあります。このモジュールを使っている時に、たまに、ある時間を指定してテストを実行したい時があります。そのような場合は以下のようにすれば実現できます。

use Test::Time;
use DateTime;

my $target_dt = DateTime->new(year => 2015, month => 7, day => 15);
$Test::Time::time = $target_dt->epoch;
my $now = time;


Test::Time::Atを用いると、このような操作を少し便利にする事ができます。do_atという関数を用いると、特定のブロック内だけ時間を指定して実行することが出来ます。

use Test::More;
use Test::Time time => 1;
use Test::Time::At;

is time, 1, 'Test::Timeで指定したので、ここでは1になる';

do_at {
    is time, 1000, 'Test::Time::Atによって、このブロックは1000になる';
    sleep 10;
    is time, 1010, 'ブロック内でsleepすると時間が進む';
} 1000;

is time, 1, 'ブロックを外れると1に戻る';

Time::PieceやDateTimeなど、epochというメソッドを使えるインスタンスも同様に渡すことが出来ます。

use Test::Time;
use Test::Time::At;

use Time::Piece;
use DateTime;

do_at {
    my $now = gmtime;
} Time::Piece->strptime('2015-08-10T06:29:00', '%Y-%m-%dT%H:%M:%S');

do_at {
    my $now = DateTime->now;
} DateTime->new(year => 2015, month => 8, day => 10);

subtest内の時間を指定する場合は、sub_atを利用することでネストを浅くすることも出来ます。

use Test::More;
use Test::Time;
use Test::Time::At;

subtest 'この中の時間を指定したい' => sub_at {
    is time, 1000;
} 1000;


どうぞご利用ください。
https://metacpan.org/pod/Test::Time::At
https://github.com/hatena/Test-Time-At

TheSchwartzが次に実行するjobをどう見つけるか

TheSchwartzを利用していて、priority周りがどのように機能しているのかよく分からなかったので、軽くコードを読んでみた。

TheSchwartzでジョブを一つ実行する一番単純な手段はTheSchwartz->work_onceを一度実行する部分である。またジョブを実行するための他のいろいろな関数でも結局このwork_onceを利用している。そこでまずはwork_onceから読んでみる。

work_once

https://metacpan.org/source/JFEARN/TheSchwartz-1.12/lib/TheSchwartz.pm#L646

work_onceを見ると、すぐにjobを探していると思われるメソッドが見つかる。work_onceを引数なしで呼ぶことにより、find_job_for_workersが呼ばれ、jobが一つ返ってくるようだ。そこで続いてこのfind_job_for_workersを読んでみる。

find_job_for_workers

https://metacpan.org/source/JFEARN/TheSchwartz-1.12/lib/TheSchwartz.pm#L328

find_job_for_workersで重要そうな部分は

辺り。

https://metacpan.org/source/JFEARN/TheSchwartz-1.12/lib/TheSchwartz.pm#L327 にかかれているように、一度に取得してくるジョブ数は$client->batch_size分であり、これはデフォルトでは50に設定されている。

またhttps://metacpan.org/source/JFEARN/TheSchwartz-1.12/lib/TheSchwartz.pm#L329 にあるように、prioritizeというオプションが有効であれば、priority -> jobidの順でsortしてDBから取得してくるという事がわかる。prioritizeが有効でなければjobidの順でDBから取得してくる。なので、https://metacpan.org/pod/TheSchwartz に書かれているprioritizeというオプションが有効でないと、いくらjobにpriorityを指定しても意味が無いという事がわかる。

最後にhttps://metacpan.org/source/JFEARN/TheSchwartz-1.12/lib/TheSchwartz.pm#L380 から、$client->_grab_a_jobで、取得してきたjobから実行するjobを選んでいることが分かる。なので次は_grab_a_jobを読んでみる。

_grab_a_job

https://metacpan.org/source/JFEARN/TheSchwartz-1.12/lib/TheSchwartz.pm#L397

_grab_a_jobを見ると、まず最初に渡された引数をshuffleしている。つまり先ほど取ってきた50個ほどのジョブをshuffleしている。その後、順にjobを見ていって他のclientに取られていないjobを返すようにしている。基本的にはそれぞれのclientで別々にshuffleされているため、大体は一番最初のjobが実行されるように見える。

結論

  • prioritizeが有効であれば、priorityが高いものを優先してDBから取得してきてくれる
  • ただし、DBから取得してきたあとに、実行するjobを決めるときにはshuffleするため、priorityが高いものが常に先に実行されるわけではない
  • 極論を言うと、priorityが低いjobが1000個くらい入っていて、priorityが高いjobが3個という状況の時、1つジョブを選ぶ時にpriorityが高いものは3/50の確率でしか選ばれない
  • またDBから取得する際のデフォルトのlimitは50なので、ある程度ジョブがはけていて50個以下しかない場合は、priorityは意味が無い

などといったことが分かった。