12 Punkte von GN⁺ 2025-05-27 | 1 Kommentare | Auf WhatsApp teilen
  • Wenn in einem Bash-Skript zur Prüfung des Status eines Webservers wiederholt Verbindungsversuche ausgeführt werden, kann das Problem auftreten, dass der Server unerwartet in einer Endlosschleife hängen bleibt
  • timeout, ein Werkzeug zur Lösung dieses Problems, legt eine maximale Laufzeit für einen Befehl fest und versucht bei Überschreitung durch Senden eines Signals, den Prozess zu beenden
  • Auf Shell-Built-ins wie until lässt es sich nicht direkt anwenden; das Problem kann jedoch durch Wrapping in einen bash-Prozess oder das Auslagern in ein separates Skript gelöst werden

Warten auf einen Webserver in Bash-Skripten und das Problem der Endlosschleife

  • In der Praxis werden Bash-Skripte genutzt, um Webserver einzurichten und ihren Status zu prüfen
  • Während der Server hochfährt, werden nachfolgende Schritte zurückgestellt; grundsätzlich funktioniert das ohne Probleme
  • Wenn der Server beim Start jedoch abstürzt, gerät das Skript in eine Endlosschleife, sodass eine Lösung nötig wurde

Beispiel für die Nutzung von until und seine Grenzen

  • Mit einer Anweisung wie der folgenden wird der Health-Check des Webservers wiederholt ausgeführt
    until curl --silent --fail-with-body 10.0.0.1:8080/health; do  
    	sleep 1  
    done  
    
  • Wenn der Server fehlschlägt, entsteht die Situation, dass sleep 1 endlos wiederholt wird

Einführung des timeout-Dienstprogramms

  • Der Befehl timeout beendet einen Prozess, indem er ein Signal (z. B. SIGTERM) sendet, wenn der angegebene Befehl nicht innerhalb der vorgegebenen Zeit abgeschlossen wird
  • Beispiel: Bei timeout 1s sleep 5 wird nach 1 Sekunde versucht, den sleep-Prozess zu beenden
  • Beim Beenden wird ein Fehlercode (z. B. 124) zurückgegeben

Versuch, timeout mit until zu kombinieren, und das Problem dabei

  • Naheliegend ist daher der Versuch, timeout und until wie folgt zu kombinieren
    timeout 1m until curl ...; do  
    	sleep 1  
    done  
    
  • timeout kann jedoch nur Signale an Prozesse senden, während until ein in die Shell eingebautes Schlüsselwort ist und daher nicht direkt anwendbar ist

Lösung: Wrapping in einen Bash-Prozess oder Nutzung eines externen Skripts

  • Wenn die gesamte until-Schleife mit bash -c umschlossen und als separater Prozess ausgeführt wird, kann timeout darauf angewendet werden
    timeout 1m bash -c "until curl ...; do sleep 1; done"  
    
  • Alternativ kann der Schleifenteil in ein externes Bash-Skript ausgelagert werden, auf das dann timeout angewendet wird
    timeout 1m ./until.sh  
    
  • Zwar lässt sich timeout nicht direkt auf Shell-Built-ins anwenden, aber mit diesen Methoden kann das gewünschte Verhalten erreicht werden

1 Kommentare

 
GN⁺ 2025-05-27
Hacker-News-Kommentare
  • Einer meiner liebsten, wenig bekannten Tricks ist die Verwendung von strace Fault Injection, um Fehler bei verschiedenen System Calls zu testen.

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

    Im zugehörigen Link wird das ausführlicher erklärt.

    • Diese Funktion ist wirklich erstaunlich, und ich wünschte, ich hätte sie schon früher gekannt.
      Weil es keine Möglichkeit gab, Fehlerzweige zu testen, habe ich manchmal Teile einer Funktion vorübergehend durch Testcode ersetzt; mit diesem Trick scheint ein viel eleganterer Ansatz möglich zu sein.

    • Die Methode wirkt wirklich nützlich.
      Ich frage mich, ob es unter Windows etwas Vergleichbares gibt.

  • Für Service-Health-Checks wird vorgeschlagen, sowohl eine maximale Timeout-Dauer als auch eine maximale Anzahl von Wiederholungen festzulegen.
    Üblicherweise versucht man es bis zu X-mal erneut und betrachtet den Check spätestens nach Y Zeit als fehlgeschlagen.
    Es wird betont, dass man möglichst schnell scheitern sollte, statt zu lange zu warten.
    Bei Standard-Services starten Health Checks erst dann, wenn Container-Abhängigkeiten ausreichend abgesichert sind und der Dienst betriebsbereit ist.
    Verweise auf Init Container in Kubernetes, dependsOn in AWS ECS und depends_on in Docker Compose.
    Ein POSIX-Shell-Skript als Beispiel wird gezeigt.
    Es wird aber auch erwähnt, dass curl diese Funktion bereits eingebaut hat und man daher statt eines separaten Skripts einfach Folgendes verwenden kann:

    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
    
  • Es wird berichtet, dass der Befehl timeout auf dem Mac standardmäßig nicht verfügbar ist und deshalb mehrfach versucht wurde, einen Timeout nur mit Bash-Builtins zu implementieren.
    Dabei wird erklärt, dass der sleep-Befehl in POSIX standardisiert ist und daher verwendet werden kann.
    Unten wird ein Beispiel für eine Timeout-Implementierung gezeigt.

    # TIMEOUT SYSTEM(Zusammenfassung)
    # function timeout <num_seconds> <command>
    # löst <command> nach einer gewissen Zeit aus
    

    Eine Funktion namens times_up übernimmt die Timeout-Behandlung.
    Außerdem wird ein Testbeispiel mit 20 Durchläufen einer for-Schleife bei 10 Sekunden Timeout gezeigt.

    • Vor 12 Jahren wurde nach einem Ratschlag auf Stack Overflow eine ähnliche Methode implementiert.
      Im Referenzlink finden sich weitere Details.
      Es wurden nur Shell-Builtins und sleep verwendet, und der Code musste unbedingt POSIX-kompatibel sein.
      Es wird darauf hingewiesen, dass die Bash-Syntax {1..20} im Beispiel nicht POSIX-konform ist.
      Die eigene Verbesserung bestand darin, bei ausbleibendem Timeout true und bei eingetretenem Timeout false zurückzugeben, damit sich Fehlerbehandlung im Skript einfacher umsetzen lässt.

    • Es wird eine sehr einfache Methode gezeigt, bei der ein Befehl und sleep parallel laufen und der Befehl nach der vorgegebenen Zeit per Signal beendet wird.

      <command> & sleep <timeout>; kill -SIGALRM %1
      
    • Es wird ein Skriptbeispiel von vor 13 Jahren geteilt, das read -t zur Umsetzung eines Timeouts verwendet.
      Link

  • Es wird darauf hingewiesen, dass curl bereits das Flag --retry-connrefused besitzt und sich diese Funktion daher auch ohne Shell-Schleife direkt nutzen lässt.

  • Wenn bei bash -c Variablen übergeben werden müssen, wird empfohlen, Argumente wie folgt hinzuzufügen:

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

    Es wird erklärt, warum "--" verwendet wird und welche Rolle argv[0] spielt.
    printf %q wäre ebenfalls möglich, aber ein Bourne-kompatibler Ansatz wird bevorzugt.

    • "--" wird als sehr klar verständliches Signal für das Ende von Optionen in Bash und den meisten Unix/Linux-CLIs beschrieben.
      Siehe auch

    • BusyBox entscheidet anhand des Werts von argv[0], welches Programm ausgeführt werden soll, daher kann man Werte wie "ls", "mv" oder "cp" setzen.

  • Wenn Wiederholungslogik benötigt wird, wird meist folgender Ansatz verwendet:

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

    Nicht besonders elegant, aber meistens korrekt; als fortgeschrittenere Variante wird exponentielles Backoff erwähnt.
    Auch hinsichtlich Erweiterbarkeit bietet das Vorteile.

    • shellcheck empfiehlt dafür, die Variable _ zu verwenden.
      Referenzlink

    • Es wird betont, dass die Funktion eventually_succeeds je nach Situation eventuell ein Timeout oder zusätzliche Schutzmaßnahmen braucht.
      Eine Erinnerung daran, in POSIX-/Prozess-/IO-Kontexten stets defensiven Code zu schreiben.

  • Es wird erzählt, dass früher, als die Kinder noch klein waren, der folgende Befehl als eine Art Kindersicherung genutzt wurde, damit sie nur 30 Minuten lang genau ein Programm ansehen konnten:

    timeout 1800 mplayer show.mp4 ; sudo pm-suspend
    

    Die Idee wird als sehr nützlich beschrieben.

    • Dazu kommt die Meinung, dies sei das am coolsten erklärte Einsatzszenario.
  • Es wird erklärt, dass man die Verwendung von Inline-Befehlen oder temporären Skriptdateien nicht besonders mag, wenn Signale an Subprozesse gesendet werden müssen.
    Bevorzugt wird ein Ansatz, bei dem die gewünschte komplexe Logik in einer Funktion gekapselt, exportiert und dann mit timeout bash -c umschlossen wird.
    Das hängt auch mit der von aidenn0 erwähnten sicheren Übergabe von Argumenten zusammen.

    #!/usr/bin/env bash
    
    long_fn () { # gewünschte Logik implementieren
     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
    
    • Es wird darauf hingewiesen, dass am Ende unbedingt "$@" verwendet werden muss.
      Andernfalls werden Argumente mit Leerzeichen nicht korrekt übergeben.
      Dazu wird ein Beispiel mit long_fn geteilt, an dem sich das überprüfen lässt.
  • Es wird an einen früheren Blogbeitrag erinnert, in dem timeout erwähnt wurde.
    Der zugehörige Blog wird für alle empfohlen, die sich eher für allgemeine Programmiersprachen als für Shell oder für die internen Abläufe interessieren.

  • Es wird von Erfahrungen berichtet, in Kubernetes-Setups Timeouts für Befehle ergänzt zu haben.
    POSIX-Shell-Skripte wie await-cmd.sh, await-http.sh und await-tcp.sh seien ausgereift und in bestimmten Situationen recht nützlich.
    Link zum Projekt