12 ポイント 投稿者 GN⁺ 2025-05-27 | 1件のコメント | WhatsAppで共有
  • Bash スクリプトで Web サーバーの状態確認 のために接続を繰り返し試みると、サーバーが予期せず 無限ループ に陥る問題が発生することがある
  • これを解決するためのツールである timeout は、コマンドの実行制限時間を設定し、超過時にシグナルを送って プロセス終了 を試みる
  • until のような shell built-in には直接適用できないため、bash プロセスでのラップ またはスクリプト分離によって対応できる

Bash スクリプトでの Web サーバー待機と無限ループの問題

  • 実務では Bash スクリプトを使って Web サーバーのセットアップと状態チェック を行っている
  • サーバーが起動するまで次の処理を保留する構成で、通常は問題なく動作する
  • しかしサーバー起動中に クラッシュ が発生すると、無限ループに陥るため対策が必要になった

until の使用例と限界

  • 次のような構文で Web サーバーのヘルスチェック を繰り返す
    until curl --silent --fail-with-body 10.0.0.1:8080/health; do  
    	sleep 1  
    done  
    
  • サーバーが失敗していると、sleep 1 が永遠に繰り返される 状況が発生する

timeout ユーティリティの導入

  • timeout コマンドは、指定した時間内にコマンドが完了しない場合、シグナル(SIGTERM など) を送って終了させる
  • 例: timeout 1s sleep 5 の場合、1 秒経過後に sleep プロセスの終了 を試みる
  • 終了時には 異常終了コード(例: 124) を返す

timeoutuntil の組み合わせの試行と問題点

  • 自然に timeoutuntil を次のように組み合わせようとする
    timeout 1m until curl ...; do  
    	sleep 1  
    done  
    
  • しかし timeoutプロセスを対象にシグナル送信 できる一方、until はシェルの組み込みキーワードであり 直接適用は不可能 である

解決方法: Bash プロセスでラップするか外部スクリプトを使う

  • until ループ全体を bash -c でラップして別プロセスとして実行すれば、timeout を適用できる
    timeout 1m bash -c "until curl ...; do sleep 1; done"  
    
  • あるいはループ部分を 外部 Bash スクリプトに分離 し、そのスクリプトに timeout を適用することもできる
    timeout 1m ./until.sh  
    
  • shell built-in には直接 timeout を適用できないが、上記の方法で望んだ動作を実現できる

1件のコメント

 
GN⁺ 2025-05-27
Hacker Newsの意見
  • 私が最も気に入っている、あまり知られていないテクニックは、strace の fault injection を使ってさまざまなシステムコールの失敗をテストする方法の紹介です。

    $ strace -e trace=clone -e fault=clone:error=EAGAIN
    

    関連リンクでさらに詳しく説明されています。

    • この機能は本当にすごいと思うし、以前から知っていればよかったという体験の共有です。
      失敗分岐をテストする方法がなく、関数の一部だけを一時的なコードに置き換えることがあったが、このテクニックのおかげでより簡潔なアプローチが可能になりそうです。

    • この方法は本当に便利そうだという意見です。
      Windows にもこれに似た機能があるのか気になるという話です。

  • サービスのヘルスチェックでは、最大タイムアウト時間と最大リトライ回数の両方を設定するのが最適な方法だという提案です。
    通常は X 回までリトライし、最大 Y 時間以内で失敗と判断します。
    長時間待ちすぎるより、できるだけ早く失敗を確定する必要性を強調しています。
    標準的なサービスでは、コンテナ依存関係が十分に保証され、動作準備が整ってから初めてヘルスチェックを開始します。
    Kubernetes では Init Container、AWS ECS では dependsOn、Docker Compose では depends_on の設定を参照しています。
    POSIX シェルスクリプトの例が示されています。
    ただし curl 自体にこの機能が組み込まれているので、別途スクリプトを書かずに次のように使えると述べています。

    curl --silent --fail-with-body --connect-timeout 5 --retry-all-errors --retry-delay 1 --retry-max-time 300 --retry 300 10.0.0.1:8080/health
    
  • Mac では timeout コマンドが標準では提供されていないため、bash ビルトインだけでタイムアウトを実装しようといろいろ試した経験の共有です。
    sleep コマンドは POSIX 標準なので使えることを説明しています。
    以下のようなタイムアウト機能の実装例を示しています。

    # TIMEOUT SYSTEM(要約)
    # function timeout <num_seconds> <command>
    # 一定時間経過後に <command> をトリガー
    

    times_up という関数でタイムアウト処理を行います。
    10 秒タイムアウトで for 文を 20 回繰り返してテストする例も示されています。

    • 12 年前に Stack Overflow の助言に従って似た方法を実装した経験の共有です。
      参考リンクで詳細を確認できます。
      shell builtins と sleep だけを使っており、そのコードでは POSIX 互換が必須だったことを強調しています。
      例にある bash の {1..20} 構文は POSIX ではないため注意が必要だと言及しています。
      自分の改善点は、タイムアウトが発生しなければ true、発生したら false を返すようにして、スクリプト内のエラー処理を簡単にできるようにしたことです。

    • 以下のようにコマンドと sleep を並列実行し、指定時間が過ぎたらシグナルでコマンドを終了させる非常に単純な方法の共有です。

      <command> & sleep <timeout>; kill -SIGALRM %1
      
    • 13 年前に read -t を使ってタイムアウトを実装したスクリプトの事例共有です。
      リンク

  • curl にはすでに --retry-connrefused フラグがあり、シェルループなしでもこの機能をそのまま使えるという案内です。

  • bash -c を使うときに変数の受け渡しが必要なら、次のように引数を追加する方法が勧められています。

    bash -c 'some command "$1" "$2"' -- "$var1" "$var2"
    

    "--" を使う理由と argv[0] の役割について説明しています。
    printf %q を使うこともできるが、Bourne 互換の方式を好むと言及しています。

    • "--" は bash およびほとんどの Unix/Linux CLI でオプション終了の合図として意味が非常に明確だと説明しています。
      関連参考

    • Busybox は argv[0] の値に基づいて実行するプログラムを決定するため、"ls""mv""cp" など任意のコマンド名に指定できるという共有です。

  • リトライロジックが必要なときに私が主によく使う方法は次のようなものです。

    for i in {0..60}; do
      true -- "$i"
      if eventually_succeeds; then break; fi
      sleep 1s
    done
    

    あまり洗練されてはいないが、たいてい正確で、さらに進めるなら指数バックオフも適用できると述べています。
    拡張性の面でも利点があります。

    • shellcheck ではこの問題を _ 変数で処理する方法を推奨しているとのことです。
      参考リンク

    • eventually_succeeds 関数は状況によって timeout や別の防御的コーディングが必要かもしれない点を強調しています。
      POSIX / プロセス / IO では常に防御的なコードを書く必要があることを改めて示しています。

  • 昔、子どもたちが小さかったころ、30 分間だけ番組を 1 本見られるように、次のコマンドを一種のペアレンタルコントロールとして使っていた経験の共有です。

    timeout 1800 mplayer show.mp4 ; sudo pm-suspend
    

    このアイデアはとても便利だったという評価です。

    • この用途が最もかっこよく説明された事例だという意見の追加です。
  • サブプロセスにシグナルを送る必要があるため、コマンドのインライン記述や一時スクリプトファイルを使うのはあまり好まないと述べています。
    自分が好む方法は、必要な複雑なロジックを関数にして export し、timeout bash -c で包むやり方の提案です。
    aidenn0 が言及した引数の安全な受け渡し方法とも関係しています。

    #!/usr/bin/env bash
    
    long_fn () { # 必要なロジックを実装
     sleep $1
    }
    to () {
     local duration="$1"; shift
     local fn_name="$1"; shift
     export -f "$fn_name"
     timeout "$duration" bash -c "$fn_name"' "$@"' _ $@
    }
    
    time to 1s long_fn 5
    
    • 最後に "$@" を必ず使わなければならないと指摘しています。
      そうしないと、空白を含む引数が正しく渡されない問題が発生します。
      この点を確認できる long_fn の例も共有されています。
  • 以前 timeout に触れたブログ記事の内容を思い出したという話です。
    関連ブログでは、シェルではなく一般的なプログラミング言語や内部動作原理にさらに興味がある場合の参考として勧めています。

  • Kubernetes の設定でコマンドタイムアウトを追加した経験の共有です。
    await-cmd.shawait-http.shawait-tcp.sh のような POSIX シェルスクリプトは成熟しており、特定の状況ではかなり有用に使えると案内しています。
    関連プロジェクトリンク