granja: el lock ya no se lo puede quedar un proceso fugado

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
This commit is contained in:
Sergio
2026-09-08 16:34:55 +00:00
co-authored by Claude Opus 5
parent 76bf4f39a8
commit 5247821455
5 changed files with 79 additions and 14 deletions
+24 -4
View File
@@ -26,6 +26,9 @@
# ssh root@<ip> '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 <lock>` 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
+24 -4
View File
@@ -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 <lock>` 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}"
+5 -1
View File
@@ -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
+16 -4
View File
@@ -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@<ip>}"
@@ -31,10 +34,19 @@ WORKER="${1:?uso: harvest-harkaq.sh root@<ip>}"
# 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}"
+10 -1
View File
@@ -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