gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1389Shell 137Markdown 80Rust 802602 Definiciones

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

5247821455e0043f3aca23e9f7a6d20cabd318d6 · por Sergio · (hace 1 mes) · padre 76bf4f39a · ver el árbol en este commit

Delta semántico

Ninguna definición cambió: el cambio es de texto, documentación o datos.

5 archivos · +79 −14

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@<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
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 <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}"
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@<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}"
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