シェルスクリプトでコマンドを制限時間(タイムアウト)付きで実行させる

2022-04-10Bash

はじめに

コマンドやシェルスクリプトを制限時間付きで実行させ、制限時間を超えたらタイムアウトさせたい場合があります。

例えば、特定のファイルが作成されるまで待ち続けますが、そのファイルを作成する別プロセスがエラー終了してしまい永遠に待ち続けてしまう、といったようなケースに利用できます。

検証環境

$ uname -moi
x86_64 MacBookPro16,1 Darwin

$ bash -version | head -n 1
GNU bash, バージョン 5.1.4(1)-release (x86_64-apple-darwin20.2.0)

$ timeout --version | head -n 2
timeout (GNU coreutils) 8.22
Copyright (C) 2013 Free Software Foundation, Inc.

timeout コマンドを利用する

timeout コマンドを利用することでコマンド実行にタイムアウトの設定をすることができます。

timeout コマンドを通して sleep コマンドを実行し、挙動を見ていきましょう。

制限時間(3 秒)内に終了する場合

timeout コマンドは、第 1 引数に制限時間(デフォルトの単位は秒)、第 2 引数に実行するコマンドを指定します。

3 秒の制限時間を指定する場合には、 timeout 3 <COMMAND> のように指定します。

ここでは、 sleep 2 コマンドを実行し、2 秒間だけ待った後終了するコマンドを実行させてみます。

$ timeout 3 sleep 2

$ echo $?
0

timeout コマンドの制限時間に引っかからずにコマンドが終了した場合には、実行後ステータスを示す変数 $? は 0 (正常終了) となります。

制限時間(3 秒)外に終了する場合

次に、 sleep 4 コマンドを実行し、4 秒間だけ待った後終了するコマンドを実行させてみます。
3 秒の制限時間に引っかかるはずです。

$ timeout 3 sleep 4

$ echo $?
124

timeout コマンドの制限時間に引っかってコマンドが終了した場合には、実行後ステータスを示す変数 $? は 124 となります。

timeout コマンドはどうやって実行コマンドを停止させている?

timeout コマンドの第 2 引数に指定する「実行コマンド」ですが、どうやって停止させているのでしょうか?

これは、 SIGTERM シグナルを送信して停止しています。シグナルについては以下のエントリでも取り扱っているのでご参考ください。

本当に SIGTERM シグナルが送られているのかを確認してみます。

以下のようなスクリプト( test.sh )を作成します。

#!/usr/bin/env bash
set -o errexit

trap 'echo start sigterm trap; sleep 5; echo end sigterm trap' SIGTERM

echo start
sleep 5
echo end

実行すると以下のような結果が得られます。 start の文字列が出力された後、5 秒待って end の文字列が出力されます。

$ ./test.sh
start
end

こちらのスクリプトを timeout コマンドを使って 3 秒タイムアウトを設定して実行します。

$ timeout 3 ./test.sh
start
Terminated
start sigterm trap
end sigterm trap

start が出力されてから 3 秒後にタイムアウトとなり、 ./test.sh に対して SIGTERM シグナルが送られます。 ( なぜこんな面倒なことをしているかは後述 )
./test.sh では、 SIGTERM シグナルをトラップして追加の処理を行うようにしています。

  1. start sigterm trap と出力
  2. 5 秒待つ
  3. end sigterm trap と出力

想定通りの挙動を確認できました。

SIGTERM シグナル送信後の終了処理にもタイムアウトを設定したい

先の ./test.sh のケースでは、 SIGTERM シグナル送信後も 5 秒待たされてしまいます。
SIGTERM シグナル送信後の処理についても制限時間を設定したい場合には -k オプションを付与します。

以下のコマンドでは、 ./test.sh スクリプトの終了を 最長 3 秒 待ちます。超過時は SIGTERM シグナルを送信し終了させます。
さらに、 SIGTERM シグナル送信後に 最長 2 秒 待ちますが、超過時には SIGKILL シグナルを送信し強制終了させます。

$ timeout -k 2 3 ./test.sh
start
Terminated
start sigterm trap
Killed

ひとこと

こちらも上手に利用すれば、うまいこと処理がかけそうな気もするのですが、今の所業務コードで利用したことはありません。
curl にもタイムアウトオプションがありますし、明確なタイムアウトを設定するようなロジックはあまりシェルでは書かないですね。他のプログラミング言語を使うことが多いでしょう。

2022-04-10Bash