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