solo という名前の perl script を、App::solo という名でリリースしました。

プロセス間の期限付き排他ロック - (ひ)メモ
  • プロセス間の排他的実行を制御したい
  • 一定時間経過したら実行できるようにしたい

これに対する別解答です。

以下、PODの抄訳。

NAME
    solo - run only one process up to given timeout.
SYNOPSIS
    solo -t seconds [-P pidfile] [-K signal] cmd ...

DESCRIPTION
    cmd以降で指定されたコマンドを、一定時間排他的に実行します。
    一定時間を過ぎたらコマンドにSIGTERMを送ります。

    cmdがすでに実行中の場合、実行中のコマンドのPIDの入った
    エラーメッセージを出力された後、cmdを実行することなく終了します。
    
    前回のセッションが異常終了していた場合には、前回のセッションの終了結果
    をエラーメッセージとして出力した後cmdを実行することなく終了します。
 これはタイムアウトの場合を含みます。

    -f
      前回のセッションが異常終了していた場合にも強制的にコマンドを実行します。
      この場合にもセッションが進行中の場合にはsoloは実行を拒否する点に留意して
      ください。

    -t seconds
      時間を秒で指定します。省略された場合、86400(=1日)が指定されます。
      Time::HiResのおかげで小数点以下の指定も可能です。

    -P pidfile
      PIDファイルの位置を指定します。デフォルトでは root権限で実行された場合は
      "/var/run/cmd.pid" を、それ以外では"/var/tmp/cmd.pid"を用います

    -K signal
      デフォルトのSIGTERM以外のシグナルを指定します。
実行例
% cat sleep.pl
#!/usr/bin/env perl
use strict;
use warnings;
my $seconds = shift || 1;
while ($seconds > 0) {
    print $seconds--, "\n";
    sleep 1;
}    
% ./sleep.pl 3
3
2
1
% solo -t 2 ./sleep.pl 3
3
2
./sleep.pl: Operation timed out
% solo -t 2 ./sleep.pl 3
./sleep.pl: Last session ended abnormally: Operation timed out (60).
% solo -f -t 4 ./sleep.pl 3
3
2
1
% solo ./sleep.pl 3
3
2
1

インストール

cpanminusがgitに対応したおかげで、以下のようにして一発でインストールできます。

% cpanm git://github.com/dankogai/p5-app-solo.git

miyagawa++;

カジュアルかつUnix依存がやや強いコマンドなので、CPANには上げないでおきます。

実装とその理由

例えば、フェイルオーバーを実行するスクリプトは、何度も実行できるとフェイルオーバー/バックを繰り返してフラップするので、一度フェイルオーバーしたら一定時間は実行できないようにしたい

ということであれば、一定時間内はそのコマンドはそもそも複数実行できないようにした方がよいですし、逆に一定時間を経ずに終了したのであれば即座に再実行できるようになっていればなお望ましいわけです。

soloでは、それをこのようにして実行しています。

  • PIDファイルをチェック。存在していればエラーメッセージを表示して終了。
  • forkした後、コマンドを子プロセスでexecし、PIDファイルを作成
  • 親プロセスは、自分自身にalarmをかけた後、子プロセスをwait。
    • 自分自身がalarmを受け取ったら、即座に子プロセスをkill
  • PIDファイルを削除して終了。

以下がそのキモの部分になります。

eval {
    local $SIG{ALRM} = sub { die ETIMEDOUT, "\n" };
    alarm $timeout;
    $cpid = fork();
    die $! unless defined $cpid;
    if ($cpid) {
        print {$pidfh} $cpid, "\n";
        close $pidfh;
        wait;
    }
    else {
        exec $cmd, @ARGV;
    }
    alarm 0;
};
cleanup(($? || $@);

実はこれ、llevalでも使っているテクニックで、Perl Cookbookの第16章にも載っています。IPCがらみの情報は難しい上にネットでも結構探しにくいので、一部手元においておくと重宝します。英語のKindle版では2,000円を切ってますし。

Enjoy!

Dan the Man with Too Many Processes to Kill