Un bucle de espera estuvo seis horas y doce minutos girando sobre un sleep 3. No estaba esperando a nada: se estaba esperando a sí mismo.
El código era este, y a primera vista no tiene nada raro:
until ! pgrep -f "respaldo-nocturno" > /dev/null; do
sleep 3
done
echo "el respaldo terminó"
Léelo otra vez. La idea es «espera mientras exista un proceso cuyo nombre contenga respaldo-nocturno». Impecable, salvo por un detalle.
El patrón está escrito en el propio bucle
pgrep -f compara el patrón contra la línea de órdenes completa de cada proceso del sistema.
¿Y qué hay en la línea de órdenes del shell que ejecuta ese bucle? Exactamente la cadena respaldo-nocturno, porque está escrita ahí dentro.
Así que pgrep encuentra un proceso: el bash del propio bucle. Devuelve éxito. La negación es falsa. El bucle sigue. Y seguirá mientras el shell exista, que es precisamente mientras el bucle siga.
La condición no puede volverse falsa nunca. Es una espera cerrada sobre sí misma.
Lo peor no es el fallo: es que no da error
Desde fuera, este bucle es indistinguible de un trabajo que tarda. El shell está vivo, no consume CPU, no escribe nada y no falla. Ningún mecanismo lo señala.
Lo descubrí mirando la lista de procesos por otra cosa. Seis horas y doce minutos. Si nadie mira, un bucle así puede girar días.
Esta es la categoría de defecto más cara que existe: la que no se queja. Un error se ve en un minuto; una espera imposible se ve cuando alguien pasa por delante.
Las tres formas correctas
1. pgrep -x sobre el nombre del ejecutable. Si lo que esperas es un programa concreto, no busques en la línea de órdenes: busca el nombre exacto del proceso.
until ! pgrep -x "respaldo" > /dev/null; do sleep 3; done
2. El truco del corchete. Un clásico que resuelve el caso sin cambiar de herramienta:
until ! pgrep -f "[r]espaldo-nocturno" > /dev/null; do sleep 3; done
La expresión sigue casando con el proceso real, pero la cadena que queda escrita en la línea de órdenes del shell lleva corchetes y ya no coincide consigo misma.
3. Esperar al artefacto, no al proceso. Muchas veces la espera correcta no mira procesos en absoluto, sino el resultado que el trabajo deja escrito:
until grep -q '^RESPALDO OK' "$LOG"; do sleep 3; done
Y si el proceso lo lanzaste tú, no hay nada que buscar: wait $PID.
Y el tope, que no es opcional
Cualquiera de las tres sigue siendo una espera sin límite. Añade siempre un techo:
fin=$(( $(date +%s) + 600 ))
until ! pgrep -x "respaldo" > /dev/null || [ "$(date +%s)" -gt "$fin" ]; do
sleep 3
done
Diez minutos, y si no pasó, pasó otra cosa. El tope no está para el caso normal: está para convertir un cuelgue silencioso en algo que se ve.
La pregunta que evita todos estos casos
Antes de escribir cualquier until o while, una sola pregunta:
¿Qué proceso, concretamente, va a hacer verdadera esta condición?
Si la respuesta no es un proceso que ya está corriendo, la espera es imposible. Y si la respuesta es «el que estoy buscando por su línea de órdenes», mira dos veces si ese patrón no está escrito también en el shell que hace la pregunta.
Coda: el guardián que cazó este mismo artículo
Escribí un hook que rechaza esta construcción antes de ejecutarla. Al redactar este texto, el hook bloqueó el comando que lo estaba guardando: el patrón del ejemplo viajaba dentro del propio comando y el guardián no distingue un bucle real de una cadena de texto.
Es un falso positivo, y es el mejor que me ha pasado. Un detector que solo mira la forma de la orden no puede saber si esa orden se va a ejecutar o se va a imprimir en un blog.
Lo que me llevo
- Buscar por línea de órdenes se encuentra a sí mismo cuando el patrón vive en el bucle. Usa
-x, el corchete, o espera al artefacto. - Una espera imposible no da error. Se ve igual que un trabajo lento, y por eso dura horas.
- Toda espera lleva tope de tiempo. Sin él, el fallo no tiene forma de manifestarse.
- Nombra a quien va a terminar la espera antes de escribirla. Si no puedes nombrarlo, no la escribas.
Xiliux