12 points par GN⁺ 2025-05-27 | 1 commentaires | Partager sur WhatsApp
  • Lorsqu’un script Bash effectue des tentatives de connexion répétées pour vérifier l’état d’un serveur web, le serveur peut de façon inattendue entrer dans une boucle infinie
  • timeout, l’outil qui permet de résoudre ce problème, définit une durée limite d’exécution pour une commande et, en cas de dépassement, envoie un signal pour tenter de terminer le processus
  • Il ne peut pas être appliqué directement à des shell built-ins comme until, mais on peut contourner cela en encapsulant dans un processus bash ou en séparant la logique dans un script

Attente d’un serveur web et problème de boucle infinie dans un script Bash

  • En pratique, des scripts Bash sont utilisés pour configurer un serveur web et vérifier son état
  • La structure consiste à suspendre l’étape suivante tant que le serveur n’est pas démarré, ce qui fonctionne normalement sans problème
  • Mais si le serveur plante pendant son démarrage, le script peut entrer dans une boucle infinie, d’où la nécessité de corriger ce comportement

Exemple d’utilisation de until et ses limites

  • La vérification de santé du serveur web est répétée avec une syntaxe comme celle-ci
    until curl --silent --fail-with-body 10.0.0.1:8080/health; do  
    	sleep 1  
    done  
    
  • Lorsque le serveur échoue, sleep 1 se répète alors indéfiniment

Introduction de l’utilitaire timeout

  • La commande timeout termine une commande en lui envoyant un signal (SIGTERM, etc.) si elle ne se termine pas dans le délai imparti
  • Exemple : avec timeout 1s sleep 5, une tentative de terminer le processus sleep est effectuée au bout d’une seconde
  • À la fin, elle renvoie un code de sortie anormal (par ex. 124)

Tentative de combinaison de timeout et until, et problème rencontré

  • Il est naturel d’essayer de combiner timeout et until comme ci-dessous
    timeout 1m until curl ...; do  
    	sleep 1  
    done  
    
  • Mais timeout peut envoyer des signaux à un processus, alors que until est un mot-clé intégré au shell, ce qui empêche de l’appliquer directement

Solution : encapsuler dans un processus Bash ou utiliser un script externe

  • On peut appliquer timeout à l’ensemble de la boucle until en l’exécutant dans un processus séparé avec bash -c
    timeout 1m bash -c "until curl ...; do sleep 1; done"  
    
  • Autre possibilité : déplacer la boucle dans un script Bash externe, puis appliquer timeout à ce script
    timeout 1m ./until.sh  
    
  • Même si timeout ne peut pas s’appliquer directement à un shell built-in, ces méthodes permettent d’obtenir le comportement souhaité

1 commentaires

 
GN⁺ 2025-05-27
Avis Hacker News
  • Mon astuce méconnue préférée est l’injection de pannes avec strace, qui permet de tester différents échecs d’appels système

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

    Plus de détails dans ce lien connexe

    • Je trouve cette fonctionnalité vraiment incroyable, et j’aurais aimé la connaître plus tôt
      Comme je n’avais aucun moyen de tester les branches d’échec, je remplaçais temporairement des parties de fonctions par du code provisoire, mais cette astuce semble permettre une approche plus concise

    • Cette méthode a l’air vraiment utile
      Je me demande s’il existe quelque chose de similaire sous Windows

  • Pour les vérifications d’état d’un service, il est suggéré que la meilleure approche consiste à définir à la fois une durée maximale de timeout et un nombre maximal de tentatives
    En général, on réessaie jusqu’à X fois, puis on considère l’opération en échec après au plus Y unités de temps
    Il est souligné qu’il faut décider de l’échec aussi vite que possible plutôt que d’attendre trop longtemps
    Dans les services standard, les dépendances de conteneurs sont suffisamment garanties et les health checks ne démarrent qu’une fois que tout est prêt à fonctionner
    Voir Init Container dans Kubernetes, dependsOn dans AWS ECS, et depends_on dans Docker Compose
    Un exemple de script shell POSIX est fourni
    Mais il est aussi mentionné que curl intègre déjà cette fonctionnalité, ce qui permet de l’utiliser directement ainsi sans script séparé

    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
    
  • Sur Mac, la commande timeout n’est pas fournie par défaut, donc quelqu’un a raconté avoir fait plusieurs essais pour implémenter un timeout uniquement avec des builtins bash
    Il est expliqué que la commande sleep est standard en POSIX et peut donc être utilisée
    Un exemple d’implémentation de timeout comme ci-dessous est donné

    # TIMEOUT SYSTEM(résumé)
    # function timeout <num_seconds> <command>
    # déclenche <command> après un certain temps
    

    Une fonction times_up gère le timeout
    Un exemple de test répète une boucle for 20 fois avec un timeout de 10 secondes

    • Quelqu’un a partagé avoir implémenté une méthode similaire il y a 12 ans en suivant un conseil sur Stack Overflow
      Plus de détails dans ce lien de référence
      Il a insisté sur le fait que le code n’utilisait que des builtins shell et sleep, et qu’il devait impérativement rester compatible POSIX
      Il signale qu’il faut faire attention, car la syntaxe bash {1..20} de l’exemple n’est pas POSIX
      Son amélioration consistait à faire renvoyer true si le timeout ne se produit pas, et false s’il se produit, afin de simplifier la gestion d’erreurs dans le script

    • Une méthode très simple est partagée ci-dessous : exécuter la commande et sleep en parallèle, puis arrêter la commande avec un signal une fois le délai écoulé

      <command> & sleep <timeout>; kill -SIGALRM %1
      
    • Un exemple de script d’il y a 13 ans utilisant read -t pour implémenter un timeout est également partagé
      Lien

  • Il est indiqué que curl dispose déjà du drapeau --retry-connrefused, ce qui permet d’utiliser directement cette fonctionnalité sans boucle shell

  • Lorsqu’on utilise bash -c et qu’il faut passer des variables, il est recommandé d’ajouter les arguments de la manière suivante

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

    Une explication est donnée sur la raison d’utiliser "--" et sur le rôle de argv[0]
    Il est aussi mentionné qu’on peut utiliser printf %q, mais qu’une approche compatible Bourne est préférée

    • Il est expliqué que "--" est très clairement compris comme un marqueur de fin d’options par bash et par la plupart des CLI Unix/Linux
      Référence associée

    • Busybox détermine le programme à exécuter à partir de la valeur de argv[0], ce qui permet de le définir comme ls, mv, cp, etc.

  • Lorsqu’une logique de répétition est nécessaire, voici la méthode généralement utilisée

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

    Ce n’est pas très élégant, mais c’est généralement correct, et à un niveau plus avancé on peut appliquer un backoff exponentiel
    Il est aussi mentionné que cela présente des avantages en matière d’extensibilité

    • ShellCheck recommande dans ce cas d’utiliser la variable _ pour gérer le problème
      Lien de référence

    • Il est souligné que la fonction eventually_succeeds peut, selon le contexte, avoir besoin d’un timeout ou d’un code défensif supplémentaire
      Rappel qu’il faut toujours écrire du code défensif en POSIX / processus / E/S

  • Quelqu’un a raconté qu’autrefois, lorsque ses enfants étaient petits, il utilisait la commande ci-dessous comme une sorte d’outil de contrôle parental pour limiter le visionnage à un seul programme de 30 minutes

    timeout 1800 mplayer show.mp4 ; sudo pm-suspend
    

    L’idée est jugée très utile

    • Un autre avis ajoute que c’est l’exemple d’usage le plus sympa présenté ici
  • Comme il faut envoyer des signaux à un sous-processus, quelqu’un dit ne pas trop aimer utiliser une commande inline ou un fichier de script temporaire
    Sa méthode préférée consiste à mettre la logique souhaitée dans une fonction, à l’exporter, puis à l’envelopper avec timeout bash -c
    Cela rejoint la méthode sûre de passage d’arguments mentionnée par aidenn0

    #!/usr/bin/env bash
    
    long_fn () { # implémente la logique voulue
     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
    
    • Il est signalé qu’il faut absolument écrire "$@" à la fin
      Sinon, les arguments contenant des espaces ne sont pas transmis correctement
      Un exemple avec long_fn est partagé pour le vérifier
  • Quelqu’un rappelle un ancien billet de blog où timeout était mentionné
    Le blog associé est recommandé à ceux qui s’intéressent davantage aux langages de programmation généraux qu’au shell, ou au fonctionnement interne

  • Dans un environnement Kubernetes, quelqu’un partage avoir ajouté des timeouts à des commandes
    Des scripts shell POSIX comme await-cmd.sh, await-http.sh et await-tcp.sh sont présentés comme mûrs et assez utiles dans certaines situations
    Lien vers le projet associé