Bashスクリプトで時間がかかりすぎないように timeout を使う TIL
引用元:https://news.ycombinator.com/item?id=44096395
straceでシステムコールの失敗をテストする裏技、超お気に入りなんだ。
例えばこんな感じ→ strace -e trace=clone -e fault=clone:error=EAGAIN
参考リンクも貼っとくね。
これ、マジやばいね!もっと早く知りたかったよー。今まで失敗パターンをテストできなくて、無理やりスタブで逃げてたんだけど、範囲を限定するのに必死だったんだ。
サンキュー!
これも好きかもね。
https://github.com/eradman/entr
WindowsだとApplication Verifierでフォールトインジェクションとか色々できるよ。(リンク略)
ただ、これはネイティブコード専用なんだ。
.NETみたいなマネージドアプリだったら、たぶん依存性注入(DI)がいい方法だよ。
失敗するかもしれないメソッドがあるコンポーネントをテストしたいなら、インターフェースのラッパー書いて、失敗するモックを注入するんだ。
JITが普通のアプリ使い方だといい感じに最適化してくれるよ。
Dtraceも同じようなこと、いやもっとすごいこと(破壊的アクションとか)できると思うよ。しかもWindowsもサポートしてるんだ。
ヘルスチェックでは、タイムアウト時間と最大リトライ回数の両方を設定するのが理想だよ。
ネットワークの問題を考えて、リトライ回数と時間で失敗を判断する方が、ただ待つより早く終わるよね。
Bashスクリプトで書く例もあるけど、curlには元々リトライ機能が付いてるから、そっちを使うのが楽ちんだよ。
–retryとか–retry-max-timeオプションで設定できるんだ。
(追記)さっきのスクリプト例、「done」の後に「exit 1」が必要だった!そうしないと最大ループ数超えてもエラーにならないからね。
あと、curlの「–retry 300」はスクリプトと合わせるなら「–retry 5」にすべきだったわ。ごめん!
Macにtimeoutコマンドがないからさ、bashの組み込みだけでtimeout作れないかなーって試行錯誤してたんだ。全部組み込みは無理だったけど、sleepコマンド(大体どこでも使えるはず)を許すなら、これでいけると思うよ。
コードはこんな感じ。
サブシェルでsleepして時間になったらメインシェルにSIGALRMを送るんだ。
メインシェルが終了する前にサブシェルを終了させるtrapも設定してるよ。
timeout関数でALRMシグナルが来たときの処理(指定コマンド)を設定して_alarmを呼ぶんだ。
実行例では10秒後に’TIME OUT!’って表示されて終了するようになってる。
ちなみに例のループは20秒かかるからタイムアウトするよ。
これさ、俺が12年前にStack Overflow見てやったやつがあるんだよ。GitHubのコードなんだけどね。
それを使えば(helpers除く)、こんな感じで書けるんだ。
timeoutをコールして、成功したか(タイムアウトしなかったか)をif文でチェックするんだ。
君のやつみたいに、俺も組み込みコマンドとsleepだけだよ。
busybox ashでも動くようにPOSIX準拠目指したんだ({1..20}はbashismだけど)。
俺の工夫はさ、タイムアウトしたらfalseを返すようにしたこと。
そしたらエラー処理がメインスクリプト内で簡単にできると思ってね。
俺は13年前にread -t使って書いたことあるよ。GitHubのこれね
https://github.com/kahing/bin/blob/master/timeout.sh
こんなシンプルなのではどうかな?
command & sleep timeout; kill -SIGALRM %1
それはまあ良いんだけど、もし既にバックグラウンドジョブが動いてたら、%1は間違ったジョブを指しちゃうよ。
%%ってのも使えるけど、コマンドがタイムアウト前に終わっちゃったら、これまた間違ったジョブを指すことになるね。
これはさ、コマンドが終わっても、タイムアウト時間分は必ずsleepしちゃうね。
ちなみにさ、curlには–retry-connrefusedってフラグがあるんだよ。
これ使うと、シェルでこのリトライ処理を自分で書かなくて済むから便利だよ。
あのね、もしbash -cで変数渡す必要があるなら、一番良い方法は引数として追加することだよ。
例えば、bash -c ’…’ – ”$1” ”$2”’ – ”$var1” ”$var2”ってやるんだ。
–を使うのが俺は好きなんだけど、最初の引数(argv[0])は$@で展開されないから、他の何かを使った方が分かりやすいと思う。
bashのprintf %qもあるけど、劇的に綺麗にならないならBourne互換を優先するかな。
> 俺は–を使うのが見た目好きなんだけど…
二重ハイフン(–)は、bashとかほとんどのUnix/Linux CLIプログラムにとって、すごく特別な意味があるんだよ。
getopts(1p)のmanページにも書いてあるけど、これはオプションの終わりを示すんだ。
オプションの終わりを示すもの:
- 最初に出てくる–引数で、オプション引数ではないもの
- オプション引数ではなく、かつ’-’で始まらない引数
- エラー
ほらね。
「見た目が好き」って言ったのはそういうこと。実際はさ、bash -c だとそうは動かないんだよ(コマンド文字列の後の引数は全部 argv に入るんだ、0からね)。でもさ、そういう風に動く他のコマンドと”馴染む”感じなんだよね。
> 「見た目が好き」って言ったのはそういうこと。実際はさ、bash -c だとそうは動かないんだよ(コマンド文字列の後の引数は全部 argv に入るんだ、0からね)。でもさ、そういう風に動く他のコマンドと”馴染む”感じなんだよね。
君の例の – はそうやって動いてないよ。bash -c ’some command ”$1” ”$2”’ – ”$var1” ”$var2”
この例だと – は $0 に割り当てられるけど、それと同時にコマンドラインスイッチの解釈を止める役割もあるんだ。例えば bash -c ’/bin/ps ”$1”’ – ”-l” は期待通りに動くけど、bash -c ’/bin/ps ”$1”’ ”-l” はそうならないよ。
> この例だと – は $0 に割り当てられるけど、それと同時にコマンドラインスイッチの解釈を止める役割もあるんだ。例えば…
それは bash の manページとも俺のテストとも違うよ。一番簡単なテストはこれ。 bash -c ’false; echo hi’ -e は ”hi” って表示するけど、 bash -e -c ’false; echo hi’ は期待通り。bash -c ’/bin/ps ”$1”’ foo ”-l” と bash -c ’/bin/ps ”$1”’ – ”-l” は同じように動く。
あー、君の言う通りだわ。俺の例で見られた変な挙動は bash とか -c ”コマンド文字列” の後の引数とは関係なかった。代わりに、俺が使ってるプラットフォームの ps が /bin/ps ’’
って呼び出された時にどう動くか、それが原因だったよ。
Busybox はさ、argv[0] を見て何を実行するか判断するんだ。だから argv[0] として ”ls” を食わせてやると ”ls” が動くってわけ(とか ”mv” とか ”cp” とか)。
俺がよくやるリトライ処理はこうだよ。
for i in {0..60}; do
true – ”$i” # shelleck surpression
if eventually_succeeds; then break; fi
sleep 1s
done
あんまりエレガントじゃないけど、まあまあ正しいかな。次のレベルは指数バックオフだね。だいたいちょっとだけ組み合わせやすさが残る感じ。
君次第だけど、Shellcheck がその問題を解決してほしいやり方は、for _ in みたいに _ を使うことだと思うよ。
https://github.com/koalaman/shellcheck/wiki/SC2034#intention…
アプリケーションによっては eventually_succeeds に timeout がまだ必要になることに注意ね。Bashとか、まあ POSIX/IO/Processes を扱うときはいつでもそうだけど、防御的なコーディングをしないとダメだよ。何やっても結果は伴うんだから。
俺はこの解決策の方が好きだな。文字列コマンドを実行する bash がないのが良いね。eventually_succeeds に簡単に timeout をつけられるし。
前はさ、timeout 1800 mplayer show.mp4 ; sudo pm-suspend
ってのを子供が小さい頃、30分だけ自動で番組を見せるための貧乏人のペアレンタルコントロールとして使ってたんだ。便利なコマンドだよね。
どっかでよんだけど、「シェルがあれば、どうにかなる」ってさ.ちょっとふざけてるけど、まあホント.こういうシンプルなコマンドとプログラマブルなシェルがくれる力はでっかいし、どれだけきょうちょうしてもたりないくらいだよ.
コマンドをちょくせつかくより、かんすうをtimeoutでじっこうできるラッパーをつかうのがすきだな.ふくざつなしょりもかんすうにいれられるしべんりだよ.かんたんなれいコードをのせとくね.(コードは省略)
まえのコメントのコードだけど、さいごのほうの$@は”$@”にしないとスペースいりのひきすうがうまくいかないよ.ためしてみてね.(コード例は省略)
もっとコメントを表示(1)
ああ、そのタイポまちがいだわ.シェルスクリプトで”$@”ってしょっちゅうかくのに、もっとちゃんとわかってるべきだった.shellcheckでもみつけられただろうしね.でも、HNのへんしゅうじかんすぎちゃったから、まちがいがのこったままだな
ちょいまえに、Kubernetesのセットアップでコマンドのtimeoutしょりをまさにいれたところだよ.このPOSIXシェルスクリプトでできたawait-cmd.shとかawait-http.shとかawait-tcp.shは、こなれてるし、いくつかのシナリオですごくべんりだよ.GitHubのリンクも貼っとくね:https://github.com/vegardit/await.sh
そういえば、timeoutコマンドってGNU Coreutilsのいちぶらしいね.きじをよんだあとに、Bashじたいのいちぶなのかはっきりしなかったんだ.
あと、きをつけてほしいんだけど、いろんなものとおなじで、timeoutコマンドとそのひきすうは、/usr/bin/timeoutとかBrewのgtimeoutとか、かんきょうによってちがうんだ(gプレフィックスはそこからきてる).BSDではどうなってるか、つかったことないからわかんないけど.
GNU Coreutilsにgプレフィックスをつけるのは、ほとんどの非Linux Unixシステムでよくあることだよ.ベースシステム(gmakeとかgtarとか、makeとかtarとか)とのコンフリクトをふせぐためなんだ.
でも、それもまためんどくさいんだよね.gプレフィックスつきのバージョンはLinuxシステムにはインストールされてないから、それらにいぞんしてるスクリプトはポータブルじゃなくなるわけ.
Thankfully bash は tolerates それ、if the script author cares なら、e.g. gnu_sed=gsed
if ! command -v $gnu_sed; then
gnu_sed=$(detector_wizardry)
fi
$gnu_sed -Ee … って。
> It’s a shame we can’t use timeout with until directly。The until keyword は part of the POSIX.2 shell specification で、timeout functionality は include してないんだ。bash に implemented することはできるけど、他の shells(Debian dash が the main concern)には portable じゃない。This is the reason that it is implemented as a separate utility。Search for ”The until loop” below to see the specification。https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V…
Of course I’m very judgemental towards people who ignore the established knowledge of the field and instead rely on social hearsay。Call me old and grumpy。
To be fair、manual で tool を learning するのは not very efficient。個人的には most utilities の manuals は detail に lacking で、reading だけだと理解 difficult。特に needing してない functionality の reading は waste of time みたいに feels する。everything Vim を fluent な人が manual once or twice でそうなったとは doubt。
Furthermore、reading about functionality は waste of time みたいに feels する
yeah、but simplified、this is how you end up with programmers that write an if statement 10 times because they don’t know what a for loop is。The waste of time still happens elsewhere。
> I doubt anybody who is fluent in everything Vim has to offer learned that by reading through the manual once or twice。The vim manual only explains the flags and how/where to access the documentation and tutorial。You don’t read it to ”become fluent” in using the TUI application、you read it to learn about how to run the program from the commandline。
If you haven’t、look at the FreeBSD man pages、when possible(most things in coreutils)。They are infinitely better than the GNU versions。
OpenBSDのmanページもおすすめだよ。
ちなみにLinuxだと、全部のmanページ(たとえばPOSIXとかGNU、Linuxとか)読みたいから、”man-all”って”man -a”にエイリアスしてるんだ。
timeoutコマンドじゃなくて、自分でシェル関数作る方法もあるよ。何やってるか見せるためにjobsとかecho入れてるけど、基本的にはバックグラウンドで実行して、指定時間待つか終わるかで見張って、時間切れならkillする感じ。
オプションでtimeoutとかsleep時間も変えられるよ。
他にも面白いテスト方法として、純粋なbash(たぶん15年くらい前のバージョンで修正必要かも)で”timeout 5 bash -c ’cat < /dev/null > /dev/tcp/google.com/80’”みたいにTCP接続テストできるよ。
google.comとかポート80はテストしたいとこに変えてね。
サーバーが起動してなかったり、ファイアウォールとかプロキシがあるとエラーになるかタイムアウトするよ。
timeoutってbashの外部プログラムなんだよね。
この記事みたいにcurl使うなら、プロセスレベルでタイムアウト管理するより、curl自体に”-m”オプションでタイムアウトさせる方がいいかもね。
いや、記事はcurlをループで呼んでるから、単に”-m”でcurlにタイムアウトさせるだけじゃダメなんだ。
ループ全体にタイムアウトが必要なんだよ。
curlにもリトライとかの設定である程度の時間続けるオプションがあるかもだけど、パッと出てこないな。
curlにはすでにそういうのをハンドリングするたくさんのオプションがあると思うよ。
いい解説付きのドキュメントも色々あるし。
たとえばこれとか→https://everything.curl.dev/usingcurl/downloads/retry.html
たぶんこの記事の前提は、findみたいに自分でタイムアウト機能持ってない他のプロセスにも適用できるってことだと思うな。
最近友達がhttps://google.github.io/zx/api見せてくれたんだけど、これめっちゃ使うの楽しいよ。
シェルにすごく近くて、LLMも結構よく知ってるんだって。
あー、これ聞くとJavaScriptのbunのshell APIを思い出すな。
[0]: https://bun.sh/docs/runtime/shell
sleep 回数を数えて、その回数を until 条件に入れれば終了できるんじゃない? timeout 使わなくてもいいし、timeout のために別の bash をサブシェルで起動する必要もないよ。
筆者の目的には合ってると思うよ。ただ、curl はサーバーの状態によってはタイムアウトに時間がかかることがあるんだよね。応答やエラーまでの最大時間を保証するにはどうやるか知りたいな。
POSIX じゃないけど、bash や zsh には時間の組み込み機能があるんだ(date コマンドより古いけどね)。EPOCHSECONDS を使ってタイムアウトを実装する例とか、より精度の高い EPOCHREALTIME を Zsh で使う方法とかを紹介してるよ。これらの変数は ”under known” (あまり知られてない)みたいだね。
curl の場合はリクエストや接続のタイムアウトパラメータがあるから、それでサーバーが生きてるか確認できるよ。でも、そういうオプションがないコマンドなら、やっぱり timeout ユーティリティを使うのが理にかなってるね。
curl には –connect-timeout と –max-time ってオプションがあるよ。でも、これらが DNS ルックアップとか TLS ハンドシェイクも考慮するのか覚えてないんだよね。回線が不安定なときはこれらのタイムアウトだけだと難しい場合があって、結局ハードリミットをかけるなら curl をラップする必要が出てくるみたい。
Linux でプロセスが完全に固まって kill できないことが本当によくあったんだ。だいたい I/O 待ちが原因みたいだけど。
そういうケースだと kill -9 でも助けにならないんだよね
そんなに簡単じゃないね。curl がハングアップしない保証はないから。ちゃんとやるならループの前に別プロセスで親を監視するコードが必要になる…でも、正直 Bash でそこまでやりたくないよね。curl がハングしないと仮定するなら、タイムスタンプ比較の方が回数カウントより timeout のエミュレーションとしては良いかな。あとは指数バックオフとか色々やりたくなるだろうけど…やっぱ Bash じゃない方がいいだろうね。
もっとコメントを表示(2)
個人的な経験から言うと、リトライが何回必要だったか出力するのをおすすめするよ。ゼロを期待してるなら特にね。
そうしないと、リトライループが不安定なサービスやネットワークの問題を隠しちゃうことがあるんだ。
昔、timeout.shを自分で作ったんだけど、めっちゃ複雑になっちゃったんだよね。関数呼び出しとか多くなると、環境を引き継ぎたいからさ。制御プロセスとかsleepプロセスとかあって、どっちが先に終わるかの競争とかあってさ…。たぶん、だから組み込みのtimeoutを無視したんだろうな…。
https://github.com/zbigg/bashfoo/blob/master/timeout.sh
これ読んで、前に書いた自分のブログ記事思い出したわ。そこで”timeout”にも触れてるんだ。
https://gaultier.github.io/blog/way_too_many_ways_to_wait_fo…
これは、シェルじゃなくて他のプログラミング言語で実装する場合とか、内部の仕組みを知りたい場合に役立つかもね。
bashより標準ライブラリが整ってない言語なんてあるのか?
bashスクリプト用のちょっとモダンな標準ライブラリを作ろうとしてるとこ、Stack Overflow以外にある?
Busyboxとかcoreutilsとか、お前のプラットフォームのユーザースペースが“標準ライブラリ”だよ。
シェルは基本、制御フローとIOのためにあるだけで、それ以外は全部コンピューター上のプログラムなんだ。
shellfire-devとかshellspecとかoils.pubとか、bashのモダンな試みはいくつかあるよ。他にもあるだろうね。シェルは幅広いユーザーがいるから、目標次第で深掘りできるポイント(インタプリタ間の移植性、他のバイナリへの依存度、ブートストラップの早い段階で動くかとか)はたくさんあるよ。俺のプロジェクトはこれ。
https://github.com/alganet/coral
https://github.com/alganet/shell-versions
https://github.com/Mosai/workshop
そこまで複雑になるなら、ほんとに頑張る価値ある?個人的には、そこまでいくとPythonとかRubyみたいな、もっと有能な言語使う方がいいと思うな。
それだけじゃなくて、bashの強みはどこにでもあることだよ。(あるいは、もっと普遍性を求めるならposix shだね。)もしbashにたくさん機能を追加し始めたら、使う場所全部に十分新しいバージョンが入ってるって確信できないと使う意味ないじゃん。それって、そもそもbashの主な使い道(俺の意見だけど、どんなUnix系システムでも動く移植可能なスクリプト)の目的を台無しにしちゃうんだよな。
PythonもRubyも、並行処理とか直列化とか、簡潔さのインターフェースがシンプルじゃないんだよ。全然だめ。それに、望ましい方向には動いてないしね。Perlなら挑戦できたかも…でもそれは別の問題だし、それでもまだ完璧じゃない。
PowerShellはもったいない機会だったな。底なしの金庫を持つ会社がリソースを大量に投入したプロジェクトなのに…結局イマイチだった。 sensibleな代替がないかと思ったけど、まだ見つかってないわ。
POSIXとSingle Unix Specificationが、まあ全部だね。
俺はシェルスクリプトたくさん書くけど、POSIX準拠にすることが多いよ。依存関係については、command
コマンドを使えば、インストールされてなくても gracefullyに失敗させられる。
Bashには標準ライブラリってなくて、組み込みコマンドと外部コマンドだけなんだよね。外部コマンドはただのツールって感じ。
’[’ (テストコマンド)でさえ、だいたい/usr/binにある外部バイナリなんだよ。
Bashの”標準ライブラリ””組み込みコマンド”、”予約語”について詳しく教えてくれてるね。組み込みコマンドはfork()/exec()なしで使える内部的なもの。予約語はループとか制御に使うキーワードだよ。多くのディストリビューションには.bashrc があって、これは標準ライブラリみたいに使えるって人もいるみたい。’[’ (テストコマンド)も、組み込みコマンドとして解釈されないスクリプトのために外部バイナリとしても存在するんだって。通常は組み込みとして使われるよ。
Bashの変なとこを知るたびにPowershellとかPythonに行きたくなるなー。Bashってマジでハッキーだよね。他の言語なら文法だけ考えればいいけど、Bashは「どうやったらやらかさないか?」って考えちゃうんだよね。複雑な処理は特にさ。
記事のアイデア、いいね! 本番環境でサイレントタイムアウトで困ったことあるよ。これってネストした非同期呼び出しとか、よくわかんない外部依存関係とか、どう対応するのかな? ログツールと連携できるともっと見やすくなりそう!
なんで timeout --signal=SIGKILL
を使わないで、extra bashでラップして殺しやすくしたの?..
記事によると、プロセスしか殺せないから、組み込みコマンドの”until”は新しいプロセスを作らないし、ダメなんだって。
retry っていう便利なツールがあって、リトライ処理が楽になるんだよ:https://github.com/minfrin/retry
僕の.bashrcにこれ入れてるよ:
function retry {
until $@; do :; done
alert
}
export -f retry
スクリプト以外ならけっこう使えるね。