fix(granja): el watchdog del worker miraba los BYTES, y lo que murió fueron los INODOS
Medido el 2026-10-07 en dev.gioser.net: `df -h` decía **90% con 20 G libres** —se lee como «va bien»— y `df -i` decía **13107200/13107200, 0 inodos libres**. El journal del servicio traía **63.000 `No space left on device` desde el 28-sep**: el worker no podía escribir ni sus propios logs, y estuvo así nueve días sin que nada gritara. Peor que el fallo es cómo se veía: el loop informaba **«BUILD-YIELD: 37/57 construyen (0 fallan)»**. Las 20 que no construyen no se cuentan como fallo **porque no puede escribir el `.fail` que las marcaría** — el mecanismo que registra el fallo lo rompe la misma causa que el fallo. O sea un ✓ FALSO, que es peor que el ✗ falso de un disco lleno: el ✗ falso manda a leer una receta sana, el ✓ falso dice que la granja va bien durante nueve días. Y este watchdog SÍ se despertó: purgó cachés Go puntualmente, por el % de bytes, con los inodos a cero. No es que el umbral envejeciera — vigilaba una magnitud distinta de la que falla. Ahora la presión es `max(% bytes, % inodos)` y se usa en los dos sitios: la ventana de post-mortem del suelo frío y la purga de cachés Go. El log **dice cuál medidor disparó** («inodos al 97%»), que no es cosmética: sin eso alguien purgaría cachés buscando bytes que no faltan. `INODE_HIGH` sale a 82 como `DISK_HIGH`, y `_presion` devuelve 0 sólo si `df` no contesta — un 0 inventado leería «holgado» justo cuando no se sabe. Comprobado contra el worker: `_presion /opt/takana` ⇒ «97 inodos» con los bytes al 84%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Semántico sayk'uy
Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.
1 qillqakuna · +32 −5
diff --git a/scripts/farm/farm-worker-loop.sh b/scripts/farm/farm-worker-loop.sh
index c4a0d5f7..4a963ada 100755
--- a/scripts/farm/farm-worker-loop.sh
+++ b/scripts/farm/farm-worker-loop.sh
@@ -36,6 +36,31 @@ mkdir -p "$HAMMER_DIR/work"
# pisa un vendor en curso, esa receta reintenta el próximo ciclo (cache-hit el resto). Sin esto, una
# tanda Go grande llena el disco de 80G y el I/O-wait dispara el load (cuello real, no CPU/RAM).
DISK_HIGH="${DISK_HIGH:-82}"
+# ⚠ Y EL MISMO UMBRAL PARA LOS INODOS, porque lo que tumbó esta caja no fueron los bytes
+# (2026-10-07, medido): `df -h` decía **90% con 20 G libres** —se lee como «va bien»— y `df -i`
+# decía **13107200/13107200, 0 libres**. El journal traía **63.000 `No space left on device` desde
+# el 28-sep**: el worker no podía escribir ni sus propios logs, y estuvo así nueve días.
+#
+# Peor que el fallo: el loop informaba «BUILD-YIELD: 37/57 construyen (0 fallan)». Las 20 que no
+# construyen no se cuentan como fallo **porque no puede escribir el `.fail` que las marcaría** — el
+# mecanismo que registra el fallo lo rompe la misma causa que el fallo, así que el síntoma es un
+# **✓ FALSO**: la granja «va bien» durante nueve días.
+#
+# Este watchdog estaba mirando el medidor equivocado: purgó cachés Go puntualmente, por el % de
+# BYTES, con los inodos a cero. No es que el umbral envejeciera: vigilaba una magnitud distinta de
+# la que falla. Ahora la presión es el MÁXIMO de las dos, y el log dice cuál disparó — si no lo
+# dijera, un día alguien purgaría cachés buscando bytes que no faltan.
+INODE_HIGH="${INODE_HIGH:-82}"
+# Presión del recurso: max(% bytes usados, % inodos usados). Un vacío devuelve vacío, no 0 — un 0
+# inventado leería «holgado» justo cuando `df` no contesta.
+_presion() {
+ local pb pi
+ pb=$(df --output=pcent "$1" 2>/dev/null | tr -dc '0-9')
+ pi=$(df --output=ipcent "$1" 2>/dev/null | tr -dc '0-9')
+ [ -z "$pb" ] && pb=0
+ [ -z "$pi" ] && pi=0
+ if [ "$pi" -gt "$pb" ]; then echo "$pi inodos"; else echo "$pb bytes"; fi
+}
# Cuántos minutos se le respetan a un árbol EN FRÍO cuando el disco NO está apretado. Es la ventana
# de post-mortem: por debajo de esto, un fallo se lleva su propia evidencia (ver protección 4).
SUELO_FRIO_MIN="${SUELO_FRIO_MIN:-120}"
@@ -74,13 +99,15 @@ SUELO_FRIO_MIN="${SUELO_FRIO_MIN:-120}"
# No se arregla subiendo el suelo a secas: este watchdog existe para que una tanda Go no llene
# el disco vendoreando, y ahí los minutos importan. Se arregla siendo agresivo SÓLO cuando el
# recurso escasea de verdad — con disco holgado, un árbol frío no le hace daño a nadie.
- usado=$(df --output=pcent "$HAMMER_DIR" 2>/dev/null | tr -dc '0-9') # pcent = USADO, no libre
+ # Presión = max(bytes, inodos). Antes era sólo `--output=pcent` (bytes) y con los inodos a
+ # cero este suelo seguía respetando la ventana de post-mortem mientras nada podía escribir.
+ set -- $(_presion "$HAMMER_DIR"); usado="$1"; cual="$2"
suelo=$SUELO_FRIO_MIN
- [ -n "$usado" ] && [ "$usado" -ge "$DISK_HIGH" ] && suelo=2 # disco apretado ⇒ como antes
+ [ -n "$usado" ] && [ "$usado" -ge "$DISK_HIGH" ] && suelo=2 # recurso apretado ⇒ como antes
[ -n "$(find "$dd" -mmin -"$suelo" -print -quit 2>/dev/null)" ] && continue
rm -rf "$dd"
done
- use=$(df --output=pcent "$HAMMER_DIR" 2>/dev/null | tr -dc '0-9')
+ set -- $(_presion "$HAMMER_DIR"); use="$1"; medidor="$2"
if [ -n "$use" ] && [ "$use" -ge "$DISK_HIGH" ]; then
# OJO: `go clean -modcache`/`-cache` borran ~/go/pkg/mod y ~/.cache/go-build ENTEROS. Ambos los
# usa `go mod vendor` (modcache p/ módulos, go-build p/ verificar el grafo). Si la purga corre
@@ -91,9 +118,9 @@ SUELO_FRIO_MIN="${SUELO_FRIO_MIN:-120}"
# un `go mod vendor` visible en este instante): difiero al próximo tick / al gap idle entre
# ciclos. El sandbox `go install` usa GOCACHE=/src + -mod=vendor ⇒ no toca estas caches del host.
if pgrep -f 'go mod vendor' >/dev/null 2>&1 || pgrep -f 'release/(takana|hammer).* build ' >/dev/null 2>&1; then
- echo "$(date -u +%FT%TZ) watchdog: disco ${use}% ≥ ${DISK_HIGH}% pero hay build en vuelo ⇒ difiero purga caches Go"
+ echo "$(date -u +%FT%TZ) watchdog: ${medidor} al ${use}% ≥ ${DISK_HIGH}% pero hay build en vuelo ⇒ difiero purga caches Go"
else
- echo "$(date -u +%FT%TZ) watchdog: disco ${use}% ≥ ${DISK_HIGH}% ⇒ purgo caches Go"
+ echo "$(date -u +%FT%TZ) watchdog: ${medidor} al ${use}% ≥ ${DISK_HIGH}% ⇒ purgo caches Go"
go clean -modcache 2>/dev/null; go clean -cache 2>/dev/null
fi
fi