12 puntos por GN⁺ 2025-05-27 | 1 comentarios | Compartir por WhatsApp
  • Al realizar intentos repetidos de conexión para verificar el estado de un servidor web en un script de Bash, puede surgir el problema de que el servidor caiga inesperadamente en un bucle infinito
  • timeout, una herramienta para resolver esto, establece un límite de tiempo para la ejecución de un comando y, si se supera, envía una señal para intentar terminar el proceso
  • No puede aplicarse directamente a shell built-ins como until, por lo que puede resolverse mediante wrapping de un proceso bash o separándolo en un script

Espera de un servidor web y problema de bucle infinito en scripts de Bash

  • En la práctica se usan scripts de Bash para configurar servidores web y comprobar su estado
  • La estructura pospone la siguiente tarea mientras el servidor termina de levantarse, y normalmente funciona sin problemas
  • Sin embargo, si el servidor se bloquea durante el arranque, termina entrando en un bucle infinito y fue necesario resolverlo

Ejemplo de uso de until y sus límites

  • Se repite un health check del servidor web con una sintaxis como la siguiente
    until curl --silent --fail-with-body 10.0.0.1:8080/health; do  
    	sleep 1  
    done  
    
  • Cuando el servidor falla, se produce una situación en la que sleep 1 se repite para siempre

Introducción de la utilidad timeout

  • El comando timeout termina el comando enviando una señal (como SIGTERM) si no finaliza dentro del tiempo especificado
  • Ejemplo: en timeout 1s sleep 5, después de 1 segundo se intenta terminar el proceso sleep
  • Al finalizar devuelve un código de salida anormal (por ejemplo, 124)

Intento de combinar timeout y until, y el problema

  • Naturalmente, se intenta combinar timeout con until de la siguiente forma
    timeout 1m until curl ...; do  
    	sleep 1  
    done  
    
  • Sin embargo, timeout puede enviar señales a procesos, pero until es una palabra clave incorporada del shell, por lo que no puede aplicarse directamente

Solución: wrapping de un proceso Bash o uso de un script externo

  • Si se envuelve todo el bucle until con bash -c para ejecutarlo como un proceso separado, sí puede aplicarse timeout
    timeout 1m bash -c "until curl ...; do sleep 1; done"  
    
  • Otra opción es separar la parte del bucle en un script externo de Bash y luego aplicar timeout a ese script
    timeout 1m ./until.sh  
    
  • Aunque timeout no puede aplicarse directamente a los shell built-ins, con estos métodos sí es posible lograr el comportamiento deseado

1 comentarios

 
GN⁺ 2025-05-27
Comentarios en Hacker News
  • Uno de mis trucos poco conocidos favoritos es usar strace fault injection para probar fallos en distintas llamadas al sistema

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

    En este enlace relacionado lo explican con más detalle.

    • Me parece una función realmente increíble, y ojalá la hubiera conocido antes.
      Como no tenía forma de probar las ramas de error, solía reemplazar temporalmente solo partes de una función con código provisional, pero este truco abre la posibilidad de un enfoque más limpio.

    • Esto se ve realmente útil.
      Me pregunto si existe algo parecido en Windows.

  • Para los health checks de servicios, se propone que la mejor manera es configurar tanto un tiempo máximo de timeout como un número máximo de reintentos.
    Normalmente se reintenta hasta X veces, y se considera fallo si no responde dentro de un máximo de Y tiempo.
    Se enfatiza que hay que decidir el fallo lo antes posible, en vez de quedarse esperando demasiado tiempo.
    En servicios estándar, el health check solo debería empezar una vez que las dependencias del contenedor estén garantizadas y todo esté listo para funcionar.
    En Kubernetes, revisar Init Container; en AWS ECS, dependsOn; y en Docker Compose, depends_on.
    Se comparte un ejemplo en shell POSIX.
    Pero también se menciona que curl ya trae esta funcionalidad integrada, así que podría usarse directamente así, sin script aparte:

    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
    
  • En Mac, como el comando timeout no viene por defecto, alguien comparte que hizo varios intentos para implementar un timeout usando solo builtins de bash.
    Explica que el comando sleep sí es estándar en POSIX, así que puede usarse.
    Comparte un ejemplo de implementación de timeout como este:

    # TIMEOUT SYSTEM(resumen)
    # function timeout <num_seconds> <command>
    # dispara <command> después de cierto tiempo
    

    Usa una función llamada times_up para manejar el timeout.
    También muestra una prueba repitiendo un for 20 veces con un timeout de 10 segundos.

    • Hace 12 años implementé algo parecido siguiendo un consejo de Stack Overflow.
      En este enlace de referencia se puede ver el detalle.
      Solo usaba shell builtins y sleep, y se recalca que ese código tenía que ser obligatoriamente compatible con POSIX.
      También se menciona que la sintaxis {1..20} de bash en el ejemplo no es POSIX, así que hay que tener cuidado.
      Mi mejora fue hacer que devolviera true si no ocurría el timeout y false si sí ocurría, para simplificar el manejo de errores dentro del script.

    • Se comparte un método muy simple como este: ejecutar el comando y sleep en paralelo, y cuando pase el tiempo indicado, terminar el comando con una señal.

      <command> & sleep <timeout>; kill -SIGALRM %1
      
    • Se comparte un ejemplo de script de hace 13 años que implementaba un timeout usando read -t.
      Enlace

  • curl ya tiene la bandera --retry-connrefused, así que esta funcionalidad puede aprovecharse directamente sin un loop en shell.

  • Si necesitas pasar variables al usar bash -c, se recomienda agregar argumentos así:

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

    También se explica por qué se usa "--" y cuál es el rol de argv[0].
    Se menciona que también podría usarse printf %q, pero se prefiere el enfoque compatible con Bourne.

    • Se explica que "--" tiene un significado muy claro como señal de fin de opciones en bash y en la mayoría de las CLI de Unix/Linux.
      Referencia relacionada

    • Busybox decide qué programa ejecutar basándose en el valor de argv[0], así que puede configurarse como "ls", "mv", "cp" u otro comando deseado.

  • Cuando se necesita lógica de reintentos, una forma que uso seguido es esta:

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

    No es muy elegante, pero normalmente funciona bien; en un nivel más avanzado, se podría aplicar backoff exponencial.
    También tiene ventajas en términos de escalabilidad.

    • shellcheck recomienda manejar ese tipo de caso usando la variable _.
      Enlace de referencia

    • También se enfatiza que la función eventually_succeeds podría necesitar su propio timeout o código defensivo adicional, según el caso.
      Es un recordatorio de que en POSIX/procesos/IO siempre conviene escribir código defensivo.

  • En el pasado, cuando mis hijos eran pequeños, usaba el siguiente comando como una especie de control parental para que solo pudieran ver un programa durante 30 minutos:

    timeout 1800 mplayer show.mp4 ; sudo pm-suspend
    

    Se comenta que fue una idea muy útil en la práctica.

    • Opinión adicional: este es el caso de uso explicado de la manera más genial.
  • Como necesito enviar señales a subprocesos, no me gusta mucho usar comandos en línea ni archivos de script temporales.
    El método que prefiero es poner la lógica compleja deseada dentro de una función, exportarla y luego envolverla con timeout bash -c.
    Esto se relaciona con el método seguro para pasar argumentos que mencionó aidenn0.

    #!/usr/bin/env bash
    
    long_fn () { # implementar la lógica deseada
     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
    
    • Se señala que al final hay que usar "$@" sí o sí.
      De lo contrario, los argumentos que contienen espacios no se pasarán correctamente.
      También se comparte un ejemplo de long_fn para comprobar ese punto.
  • Se recuerda una publicación de blog anterior donde se mencionaba timeout.
    Si alguien tiene más curiosidad sobre lenguajes de programación generales o sobre el funcionamiento interno, más allá del shell, se recomienda este blog relacionado.

  • Se comparte experiencia agregando timeout a comandos en una configuración de Kubernetes.
    Se comenta que scripts POSIX shell como await-cmd.sh, await-http.sh y await-tcp.sh ya están bastante maduros y pueden ser bastante útiles en ciertos escenarios.
    Enlace al proyecto relacionado