タイムアウト処理

処理に時間がかかりそうなスクリプトを組むとき、タイムアウト時間を設定して、それを超えても処理が完了しなければ、エラーを表示するような処理が必要になることがあります。特に、ソケットを使って、他サーバと通信する場合等があてはまります。もし相手のサーバからの応答が無かったら、どうなるでしょうか。応答があるまで、待ちつつけることになります。もしくは、ブラウザー側が見切りをつけて待機することをやめてしまう。もしお使いのサーバにて、CGI の処理時間に制限があれば、Internal Server Error となるでしょう。

ここでは、秒数を指定してタイムアウト時の処理ができるうようにする方法を解説します。

タイムアウト処理の方法

タイムアウトの処理を実装する方法として、ALRM シグナルを使う方法を解説します。

まず、シグナルとは、イベントが発生した際に、オペレーティングシステムがプロセスに伝達する信号とお考え下さい。このコーナーではタイムアウト処理を扱いますので、タイムアウトしたという事実をイベントとしてプロセスに伝えてあげる必要があります。そして、スクリプトには、そのタイムアウトしたというイベントをキャッチするようにしなければいけません。これを実現するためには、ALRM シグナルをキャッチするためのシグナルハンドラ $SIG{ALRM} と タイマーをセットして時間がきたらタイムアウトというシグナルは発するようシステムに指示する alarm 関数を使います。シグナルにはさまざまな種類があるのですが、alarm 関数は、各種シグナルのうち、ARLM というシグナルを発生させるようシステムに要求することができます。

※ タイムアウト処理ではシグナルを使うため、Windows 系サーバでは使えません。

スクリプトでは、ざっくりと以下の流れになります。

$SIG{ALRM} = \&timeout; # ALRM シグナルをキャッチした場合の処理を定義
alarm 10; # タイマーを 10 秒にセット
・・・
・・・
#タイマーで時間を監視したい処理
・・・
・・・
alarm 0; # タイマーをキャンセル

sub timeout {
#タイムアウトした際に実行する処理
}

順に処理内容を見ていきましょう。

$SIG{ALRM} = \&timeout;

ALRM シグナルをキャッチした際に実行する処理を定義します。右辺には、サブルーチンのリファレンスもしくは、無名サブルーチンを指定します。ここでは、timeout というサブルーチンのリファレンスを指定しています。

alarm 10

タイマーをセットします。この行からタイマーがスタートすることになります。この行では、alarm 関数の引数として 10 を指定しておりますが、10 秒後に ALRM シグナルを発するようシステムに要求をしていることになります。

alarm 0

タイマーをキャンセルします。これは忘れないようにして下さい。もし時間内に処理が完了しても、タイマーが動き続けていると、その時間になったら ALRM シグナルが発生し、サブルーチン timeout が実行されてしまいます。そのため、タイムアウト処理を施したい処理の最後には、タイマーをリセットするよう alarm 0; を入れるのです。

上記スクリプト例は、あくまでも流れを見ていただくためのものです。これでもタイムアウト処理は実現できますが、完全ではありません。

タイムアウトした際に実行する処理(この場合、サブルーチン timeout の処理)がただ単に、メッセージを print 文で表示するだけの場合、スクリプトが終了しないことがあります。例えば、こんなスクリプトを見てみましょう。

$SIG{ALRM} = sub { print "timeout\n" };
alarm 3;
open(FILE, ">>./$file");
flock(FILE, 2);
alarm 0;

このスクリプトを実行すると、flock が完了するまで、スクリプトが終了しません。もちろん、3 秒後には timeout と表示はされますが、その後、ずっと待ち続けてしまいます。Perl はもし可能であればシステムコールを再び試みようとするからなんです。従って、ALRM シグナルをキャッチした際の処理には、exit 関数等を使って、必ずスクリプトを終了するようにしなければいけなくなります。

しかしそうはいかない場合もあるでしょう。上記問題をクリアするために、eval 関数を使います。タイムアウトの処理をする場合には、以下の用法を使えば、トラブルも少なくなるでしょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
eval {
  local $SIG{ALRM} = sub { die "timeout" };
  alarm 10;
  ・
  ・
  #タイマーで時間を監視したい処理
  ・
  ・
  alarm 0;
};
alarm 0;
if($@) {
  if($@ =~ /timeout/) {
    # タイムアウト時の処理
  } else {
    # その他例外時の処理
  }
}

1行目から10行目にかけて、一連のタイマー処理を eval で囲みます。9行目の alarm 0 は忘れないように気をつけてください。

eval 関数の中で注目していただきたいのが2行目です。シグナルハンドラとして無名関数(サブルーチン)を指定しておりますが、その中で、die 関数を使っていることに注目してください。タイムアウトした場合、その中の処理を完全に取りやめて抜け出すために die 関数を使う必要があります。

しかし、もしあなたが CGI を作っているとしたら、ちょっと心配かもしれませんね。そうです。CGI の場合、die 関数を使って処理を終了すると、当然のことながら、Internal Server Error となってしまいますよね。しかし、ご安心ください。ここは eval 関数の中なんです。eval 関数内で die を使うと、例外が発生したとみなされ、その内容が $@ に格納されますので、スクリプトは終了しません。die で eval 関数を抜け出した後の処理は、12行目からとなります。

11行目に再度、alarm 0; がありますが、これは必要です。もし9行目の alarm 0; に到達する前に時間切れになった場合、どこかでタイマーをキャンンセルしなければいけないからです。従って、eval 関数の処理を抜けた直後にタイマーをリセットしているわけです。

12行目からは、エラーハンドリングとなります。eval 関数から抜け出した場合、何かしらのエラーがあれば、$@ にエラー内容が格納されているはずです。タイムアウトした場合には、2 行目の die 関数で指定したエラー内容がそのまま格納されているはずです。従って、$@ の内容によって、場合分けをし、それぞれの処理を記述します。