timeoutコマンド入門|終わらない処理に制限時間をつける

timeoutコマンド入門|終わらない処理に制限時間をつける

実行したコマンドが、いつまで待っても返ってこない。

ネットワーク越しの処理や定期実行のスクリプトを書いていると、この「終わらない」に必ず一度はぶつかります。

ぶら下がったままのプロセスは後続の処理を止め、ジョブそのものを詰まらせる原因になるのが厄介なところ。

そこで出番になるのが、GNU coreutils に含まれる timeout コマンドです。

制限時間を渡して実行するだけで、超えた瞬間にシグナルを送って打ち切ってくれます。

本記事の実行例は、GNU coreutils 8.32 のサンドボックス環境で実際に動かし、出力をそのまま貼りました。

timeout は「時間で打ち切る」ための道具

timeout は、指定した秒数だけコマンドを走らせ、超えたらシグナルを送って止めるだけの小さなツールです。

タイムアウト処理をスクリプト側で自前実装しようとすると、バックグラウンド実行とPID管理とスリープ監視を書く羽目になります。

その面倒を丸ごと肩代わりしてくれるのが timeout の価値

書式は驚くほど単純で、時間とコマンドを並べるだけです。

timeout 5 sleep 30

これで、最大5秒だけ sleep 30 を走らせ、超えたら止めます。

既存のコマンドの前に足すだけで済むので、シェルスクリプトへの後付けが効くのもありがたい点。

時間の指定にはサフィックスが使える

数字だけを書いた場合、単位は秒として扱われます。

サフィックスを付ければ、分や時間でも指定できました。

timeout 90 ./job.sh      # 90秒
timeout 1.5s true        # 1.5秒(小数もOK)
timeout 2m ./batch.sh    # 2分
timeout 1h ./nightly.sh  # 1時間

s(秒)/m(分)/h(時)/d(日)の4つが使えます。

小数が通るのは地味に便利で、0.5 のような短い上限もそのまま書けました。

逆に「30秒」を 30m と書き間違えると30分待たされるので、サフィックスの打ち間違いだけは注意が要ります。

終了ステータスで「時間切れ」を見分ける

打ち切ったのか、それともコマンドが自力で失敗したのか。

これを区別する鍵が終了ステータス124です。

$ timeout 2 sleep 5
$ echo $?
124

$ timeout 2 sleep 1
$ echo $?
0

時間切れなら124、時間内に終わればコマンド自身の終了ステータスがそのまま返ってきました。

125はtimeout自身の失敗、126は実行できない、127はコマンドが見つからないという割り当てになっています。

手元でも次のとおり再現できました。

$ timeout 2 /nonexistent-cmd
timeout: failed to run command '/nonexistent-cmd': No such file or directory
$ echo $?
127

$ timeout 2 /etc
timeout: failed to run command '/etc': Permission denied
$ echo $?
126

スクリプト側では「124なら時間切れ」と分岐を書いておけば、リトライと本当のエラーを切り分けられます

SIGTERM を無視されると、待ち続けてしまう

ここが timeout のいちばんの落とし穴でした。

timeout が既定で送るのはSIGTERMで、受け取った側が無視するようにできている場合、プロセスは死にません

検証用に、SIGTERM を握りつぶすスクリプトを用意します。

#!/bin/bash
trap '' TERM
sleep 30

これを2秒の制限で実行した結果が次のとおり。

$ time timeout 2 ./ignore.sh
real    0m30.002s
$ echo $?
124

2秒で打ち切ったつもりが、実際には30秒まるごと待たされました

終了ステータスは124なので、ログだけ見ると「ちゃんと2秒で止まった」と誤読しかねません。

対策が -k オプションで、SIGTERM のあと指定秒だけ待って、それでも死ななければSIGKILLを送るという追い討ちの設定です。

$ time timeout -k 1 2 ./ignore.sh
real    0m3.001s
$ echo $?
137

2秒+1秒の計3秒で終わり、SIGKILLを示す137が返りました。

止まらないと困る場面では -k をセットで書く、と決めておくのが安全です。

送るシグナルと終了ステータスは変えられる

-s を使えば、送るシグナルそのものを差し替えられます。

$ timeout -s KILL 1 sleep 5
$ echo $?
137

もうひとつ覚えておきたいのが --preserve-status

124の代わりに「シグナルで終わった」ことを示すステータスを返すオプションです。

$ timeout --preserve-status 2 sleep 5
$ echo $?
143

143は128+15、つまりSIGTERMで終了したという意味になります。

呼び出し元がシグナル由来の終了を前提に組まれているときは、こちらのほうが素直につながりました。

ただし124という分かりやすい目印は消えるので、切り替えるなら受け側の分岐もあわせて直す必要があります。

パイプの中では124が出ないことがある

実測していて気付いた挙動を、もうひとつ。

timeout をパイプの左側に置くと、下流が先に閉じたときは時間切れになりません。

$ timeout 2 yes | head -3
y
y
y
$ echo ${PIPESTATUS[0]}
141

141は128+13で、SIGPIPE を受けて終わったことを示す値です。

この場合は timeout が働いたのではなく、head が読み終えて下流を閉じただけ

パイプの中で124を期待して分岐を書くと、思ったとおりに動かないわけです。

判定したいなら $? ではなく PIPESTATUS を見る、と覚えておくと事故が減ります。

最後に

timeout は、覚えることが少ないわりに効き目の大きいコマンドでした。

まず「時間とコマンドを並べるだけ」という最小の使い方を押さえる。

次に124という目印を知り、SIGTERM を無視されうるという前提で -k を添える

この3つだけでも、終わらない処理に振り回される場面はかなり減らせます。

詳しい仕様は GNU Coreutils マニュアル と timeout(1) man page を参照してください。

timeout(1) - Linux manual page

以上です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA