From 5247821455e0043f3aca23e9f7a6d20cabd318d6 Mon Sep 17 00:00:00 2001 From: Sergio Date: Tue, 8 Sep 2026 16:34:55 +0000 Subject: [PATCH] granja: el lock ya no se lo puede quedar un proceso fugado MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El lock lo sostiene la descripción de fichero abierta y los hijos la heredan: con 'exec 9>' + 'flock 9' cualquier descendiente que sobreviva al script retiene el lock para siempre, y el siguiente que lo pida espera sin que nada falle. Medido hoy: dos firefox colgados de una caza sobrevivieron al kill de su bwrap y dejaron a la granja hora y media sin poder compilar. Dos formas de defecto ⇒ dos arreglos, y no son intercambiables: · cosecha-cron, campana-deuda, harvest-harkaq — el lock cubre el script ENTERO, así que se re-ejecutan bajo 'flock -o' (cierra el fd antes de ejecutar). Ir poniendo '9>&-' comando a comando ahí es jugar a los topos. '-E 77' separa 'estaba ocupado' de 'el trabajo falló', que con el 9> no se distinguían. · farm-worker-loop, latido — el lock vive en un subshell / es a propósito el mecanismo de vida, así que basta '9>&-' en los hijos. En latido NO se puede usar -o: ahí el fd retenido ES como --status sabe que el latido vive sin pidfile. Su modo de fallo era el peor de todos: un nieto fugado dejaba el latido MUERTO PERO APARENTANDO ESTAR VIVO. Medido antes de elegir, no deducido del manual: exec 9> + flock 9 → el nieto RETIENE (control negativo: reproduce el fallo) exec {L}> (fd auto) → el nieto RETIENE (bash NO lo marca close-on-exec) flock -o / 9>&- hijo → lock LIBRE Y probados los cinco sobre el fichero real, no sobre una maqueta: copias truncadas justo tras el bloque del lock, invocadas con RUTA RELATIVA DESDE OTRO DIRECTORIO (que es lo que rompe un $0 sin resolver — de hecho la primera versión de harvest-harkaq calculaba YO después del cd y habría fallado ahí). Los tres entran, ninguno deja el lock tomado por el nieto, y la exclusión mutua sigue funcionando con su mensaje y su exit 0 — que es lo que un arreglo de locks puede romper en silencio. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2 --- scripts/farm/campana-deuda.sh | 28 ++++++++++++++++++++++++---- scripts/farm/cosecha-cron.sh | 28 ++++++++++++++++++++++++---- scripts/farm/farm-worker-loop.sh | 6 +++++- scripts/farm/harvest-harkaq.sh | 20 ++++++++++++++++---- scripts/farm/latido.sh | 11 ++++++++++- 5 files changed, 79 insertions(+), 14 deletions(-) diff --git a/scripts/farm/campana-deuda.sh b/scripts/farm/campana-deuda.sh index 10aaaa99..1430e7ea 100755 --- a/scripts/farm/campana-deuda.sh +++ b/scripts/farm/campana-deuda.sh @@ -26,6 +26,9 @@ # ssh root@ 'journalctl -u campana-deuda -f' set -u HAMMER_DIR="${HAMMER_DIR:-/opt/hammer}" +# Ruta absoluta a MÍ MISMO antes del `cd`: acá el `cd` va a HAMMER_DIR, que ni siquiera tiene por +# qué contener a este script, así que un `$0` relativo se pierde seguro. +YO="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" cd "$HAMMER_DIR" HAMMER="${HAMMER:-$HAMMER_DIR/target/release/hammer}" STORE="${STORE:-$HAMMER_DIR/store}" @@ -63,10 +66,27 @@ ts() { date -u +%FT%TZ; } LOCK_FARM="${LOCK_FARM:-$HAMMER_DIR/work/.farm-build.lock}" LOCK_WAIT="${LOCK_WAIT:-7200}" mkdir -p "$(dirname "$LOCK_FARM")" -exec 9>"$LOCK_FARM" -if ! flock -w "$LOCK_WAIT" 9; then - echo "$(ts) campaña-deuda: el worker-loop no soltó el lock en ${LOCK_WAIT}s ⇒ salgo sin construir" | tee -a "$LOG" - exit 0 +# ⚠ EL LOCK LO SOSTIENE EL fd, Y LOS HIJOS LO HEREDAN (medido 2026-09-08). Con `exec 9>` + `flock 9` +# cualquier descendiente que se fugue retiene el lock DESPUÉS de que el script termine, y el próximo +# que lo pida espera para siempre sin que nada falle. Pasó: dos `firefox` colgados de una caza de +# bugs sobrevivieron al `kill` de su `bwrap` y dejaron a la granja hora y media sin poder compilar, +# en silencio. `fuser -v ` es lo que lo delata. +# El arreglo es `flock -o`, que cierra el fd antes de ejecutar ⇒ el lock lo sostiene SÓLO el +# proceso `flock`, y ningún nieto puede prolongarlo. Como acá la sección crítica es el script +# ENTERO, se re-ejecuta bajo `flock -o` en vez de ir poniendo `9>&-` comando por comando, que es +# jugar a los topos y se olvida uno. `-E 77` distingue «el lock estaba ocupado» de «el trabajo +# falló», que con el `9>` de antes no se podían separar. +# Comprobado con control positivo y negativo: sin `-o` el nieto retiene; con `-o` no. Y que la +# exclusión mutua SIGUE funcionando, que es lo que un arreglo de locks puede romper sin avisar. +# ⚠ Ojo: `exec {L}>` (fd automático de bash) NO sirve — bash no lo marca close-on-exec. Medido. +if [ -z "${CAMPANA_DEUDA_CON_LOCK:-}" ]; then + export CAMPANA_DEUDA_CON_LOCK=1 + rc=0; flock -w "$LOCK_WAIT" -E 77 -o "$LOCK_FARM" "$YO" "$@" || rc=$? + if [ "$rc" = 77 ]; then + echo "$(ts) campaña-deuda: el worker-loop no soltó el lock en ${LOCK_WAIT}s ⇒ salgo sin construir" | tee -a "$LOG" + exit 0 + fi + exit "$rc" fi echo "$(ts) campaña-deuda: lock de build tomado (el worker-loop espera su turno)" | tee -a "$LOG" total=$(echo "$DEUDA" | wc -w); i=0; ok=0; fail=0; falladas=""; ultimo_rc=0 diff --git a/scripts/farm/cosecha-cron.sh b/scripts/farm/cosecha-cron.sh index 97c98247..2a3e0e7c 100755 --- a/scripts/farm/cosecha-cron.sh +++ b/scripts/farm/cosecha-cron.sh @@ -25,6 +25,9 @@ set -uo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +# Ruta absoluta a MÍ MISMO, capturada ANTES del `cd`: se usa para re-ejecutarse bajo el lock, y +# un `$0` relativo dejaría de resolver en cuanto cambiamos de directorio. +YO="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" cd "$ROOT" # LOCK (2026-07-22): el latido ya no viene sólo del crontab — `latido.sh` lo lanza desde la sesión @@ -35,10 +38,27 @@ cd "$ROOT" # -n = no esperar: si ya hay un ciclo corriendo, este sobra (el próximo tick recoge lo mismo). LOCKFILE="${LOCKFILE:-$ROOT/work/.cosecha-cron.lock}" mkdir -p "$(dirname "$LOCKFILE")" -exec 9>"$LOCKFILE" -if ! flock -n 9; then - echo "── $(date -u +%FT%TZ) cosecha-cron: ya hay un ciclo en curso ⇒ salgo (no me solapo)" - exit 0 +# ⚠ EL LOCK LO SOSTIENE EL fd, Y LOS HIJOS LO HEREDAN (medido 2026-09-08). Con `exec 9>` + `flock 9` +# cualquier descendiente que se fugue retiene el lock DESPUÉS de que el script termine, y el próximo +# que lo pida espera para siempre sin que nada falle. Pasó: dos `firefox` colgados de una caza de +# bugs sobrevivieron al `kill` de su `bwrap` y dejaron a la granja hora y media sin poder compilar, +# en silencio. `fuser -v ` es lo que lo delata. +# El arreglo es `flock -o`, que cierra el fd antes de ejecutar ⇒ el lock lo sostiene SÓLO el +# proceso `flock`, y ningún nieto puede prolongarlo. Como acá la sección crítica es el script +# ENTERO, se re-ejecuta bajo `flock -o` en vez de ir poniendo `9>&-` comando por comando, que es +# jugar a los topos y se olvida uno. `-E 77` distingue «el lock estaba ocupado» de «el trabajo +# falló», que con el `9>` de antes no se podían separar. +# Comprobado con control positivo y negativo: sin `-o` el nieto retiene; con `-o` no. Y que la +# exclusión mutua SIGUE funcionando, que es lo que un arreglo de locks puede romper sin avisar. +# ⚠ Ojo: `exec {L}>` (fd automático de bash) NO sirve — bash no lo marca close-on-exec. Medido. +if [ -z "${COSECHA_CRON_CON_LOCK:-}" ]; then + export COSECHA_CRON_CON_LOCK=1 + rc=0; flock -n -E 77 -o "$LOCKFILE" "$YO" "$@" || rc=$? + if [ "$rc" = 77 ]; then + echo "── $(date -u +%FT%TZ) cosecha-cron: ya hay un ciclo en curso ⇒ salgo (no me solapo)" + exit 0 + fi + exit "$rc" fi SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}" diff --git a/scripts/farm/farm-worker-loop.sh b/scripts/farm/farm-worker-loop.sh index 27e77310..87230f27 100755 --- a/scripts/farm/farm-worker-loop.sh +++ b/scripts/farm/farm-worker-loop.sh @@ -230,7 +230,11 @@ while :; do flock 9 echo "$(date -u +%FT%TZ) lock tomado, sigo con $Q" fi - JOBS="$JOBS" PROMOTE=0 QUEUE="$Q" scripts/build-farm.sh 2>&1 | tail -50 + # `9>&-` cierra el fd DEL LOCK en el hijo: si un build deja un proceso fugado, sin esto el + # nieto retiene el lock para siempre y la granja queda muda (medido 2026-09-08; ver la + # regla 1 de CLAUDE.md). Acá el lock vive en el subshell `( … ) 9>`, así que alcanza con + # cerrarlo en los hijos y no hace falta re-ejecutar nada. + JOBS="$JOBS" PROMOTE=0 QUEUE="$Q" scripts/build-farm.sh 2>&1 9>&- | tail -50 9>&- ) 9>"$LOCK_FARM" done if [ "$total" -gt 0 ]; then diff --git a/scripts/farm/harvest-harkaq.sh b/scripts/farm/harvest-harkaq.sh index cf37fd63..93bba4a0 100755 --- a/scripts/farm/harvest-harkaq.sh +++ b/scripts/farm/harvest-harkaq.sh @@ -24,6 +24,9 @@ set -u HUB="$(cd "$(dirname "$0")/../.." && pwd)" +# Ruta absoluta a MÍ MISMO capturada ANTES del `cd`: se usa para re-ejecutarse bajo el lock, y un +# `$0` relativo dejaría de resolver en cuanto cambiamos de directorio. +YO="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" cd "$HUB" WORKER="${1:?uso: harvest-harkaq.sh root@}" @@ -31,10 +34,19 @@ WORKER="${1:?uso: harvest-harkaq.sh root@}" # corrupto. El harvest es lento (re-indexa el store por veredicto), así que un ciclo puede pisar # al siguiente. flock con -n: si ya hay una corriendo, esta sale limpia en vez de solaparse. LOCK="$HUB/work/.harvest-harkaq.lock" -exec 9>"$LOCK" -if ! flock -n 9; then - echo "$(date -u +%FT%TZ) otra cosecha en curso, salto este ciclo" - exit 0 +# ⚠ `flock -o` y re-exec, NO `exec 9>` (medido 2026-09-08): el lock lo sostiene el fd y los hijos +# lo heredan, así que un `ssh` o un `rsync` fugado de esta cosecha lo retendría DESPUÉS de que el +# script termine y todas las cosechas siguientes saldrían por «otra cosecha en curso» — mudas y +# para siempre. Con `-o` el fd se cierra antes de ejecutar y sólo el proceso `flock` lo sostiene. +# `-E 77` separa «estaba ocupado» de «el trabajo falló». Ver la regla 1 de CLAUDE.md. +if [ -z "${HARVEST_HARKAQ_CON_LOCK:-}" ]; then + export HARVEST_HARKAQ_CON_LOCK=1 + rc=0; flock -n -E 77 -o "$LOCK" "$YO" "$@" || rc=$? + if [ "$rc" = 77 ]; then + echo "$(date -u +%FT%TZ) otra cosecha en curso, salto este ciclo" + exit 0 + fi + exit "$rc" fi SSH_KEY="${SSH_KEY:-$HOME/.ssh/github5}" REMOTE="${REMOTE:-/opt/hammer}" diff --git a/scripts/farm/latido.sh b/scripts/farm/latido.sh index 848aa3ef..8549ad70 100755 --- a/scripts/farm/latido.sh +++ b/scripts/farm/latido.sh @@ -88,7 +88,16 @@ trap 'echo "── $(ts) latido termina (pid $$)" >>"$LOG"' EXIT while :; do # `|| true` + `set -uo pipefail` sin `-e`: un ciclo que falla (worker caído, red, gitea) NO puede # matar el latido. Se loguea y se reintenta al próximo tick; ésa es toda la robustez que hace falta. - ( cd "$ROOT" && ./scripts/farm/cosecha-cron.sh ) >>"$COSECHA_LOG" 2>&1 || \ + # `9>&-` cierra el fd DEL LOCK DEL LATIDO en el hijo. Sin esto, un proceso fugado de la cosecha + # lo retiene para siempre y el latido queda MUERTO PERO APARENTANDO ESTAR VIVO: `--status` lee + # el lock, lo ve tomado y dice VIVO, y `--ensure` se niega a levantar otro. Es el peor modo de + # fallo posible acá — la máquina deja de latir y todo indica que late. Medido 2026-09-08 con + # dos `firefox` colgados que retuvieron el lock de build hora y media (regla 1 de CLAUDE.md). + # ⚠ Y acá NO va `flock -o`, al revés que en cosecha-cron/campana-deuda/harvest: en este script + # el fd retenido ES el mecanismo de vida a propósito (así `--status` responde sin pidfile, que + # mentiría si el proceso muriera). Lo que sobra es que lo hereden los HIJOS, no que lo tenga el + # bucle. Por eso el arreglo es distinto aunque el defecto sea el mismo. + ( cd "$ROOT" && ./scripts/farm/cosecha-cron.sh ) 9>&- >>"$COSECHA_LOG" 2>&1 || \ echo "── $(ts) latido: el ciclo salió != 0 (ver cosecha-cron.log) — sigo" >>"$LOG" sleep "$INTERVALO" done