farm: lock compartido entre campana-deuda y el worker-loop
Los dos construyen sobre el MISMO work/, y `fetch` nombra el árbol de fuentes de forma determinista
(`work/sources/<receta>-<sha16>`, sin nada que dependa de QUIÉN construye) ⇒ dos procesos que
necesiten la misma DEP apuntan al mismo directorio: uno hace `remove_dir_all` mientras el otro
corre `tar -x`. El árbol queda a medias y devuelve "Directory not empty" (os error 39), y así se
queda hasta que alguien lo borra a mano.
No es teórico: la campaña de deuda y el loop pidieron `mesa` a la vez (el loop lo arrastraba desde
la cola KDE al invalidarse libdrm) y se llevó puesta media cascada GUI, con un error que no nombra
la causa. Casi borro ese árbol a mano antes de ver que había un bwrap montándolo.
Grano: la campaña toma el lock para toda su corrida; el loop, por ciclo de cola. Más fino rompería
el `xargs -P2` de build-farm.sh.
Dos honestidades en los comentarios, para no prometer de más:
- `flock` NO es FIFO. El loop vuelve a pedirlo enseguida y le gana a la campaña que espera:
medido, la campaña entra al terminar TODAS las colas, no entre dos. La ventana real es el
IDLE_SLEEP. Por eso espera con techo (LOCK_WAIT=7200) y sale limpia en vez de colgarse.
- Esto NO cierra la carrera del todo: el `-P2` interno del loop puede correr dos recetas de la
misma cola que compartan dep, y ésas se siguen pisando. El arreglo de fondo es un lock POR
ÁRBOL dentro de `fetch`, que cubriría los dos casos.
Verificado con flock real: exclusión mutua (la campaña entra justo cuando el loop suelta), salida
limpia por timeout con rc=0, y la no-equidad de flock medida en vez de supuesta.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
2 archivos · +44 −1
diff --git a/scripts/farm/campana-deuda.sh b/scripts/farm/campana-deuda.sh
index b989c056..df2de43c 100755
--- a/scripts/farm/campana-deuda.sh
+++ b/scripts/farm/campana-deuda.sh
@@ -41,6 +41,34 @@ DEUDA="${DEUDA:-$*}"
[ -n "$DEUDA" ] || { echo "uso: campana-deuda.sh <receta> [receta…] (o DEUDA=…)" >&2; exit 2; }
ts() { date -u +%FT%TZ; }
+
+# LOCK COMPARTIDO CON EL WORKER-LOOP (2026-07-22). Esta campaña y `farm-worker-loop.sh` construyen
+# sobre el MISMO work/, y `fetch` nombra el árbol de fuentes de forma determinista
+# (`work/sources/<receta>-<sha16>`) ⇒ dos procesos que construyan la misma DEP apuntan al mismo
+# directorio, y uno hace `remove_dir_all` mientras el otro corre `tar -x`. Eso deja el árbol a medias
+# y devuelve "Directory not empty" (os error 39) — y el árbol roto se queda así hasta que alguien lo
+# borra a mano. Pasó de verdad: la campaña y el loop pidieron `mesa` a la vez (el loop lo arrastraba
+# desde la cola KDE) y se llevó puesta media cascada GUI.
+#
+# El lock es EXCLUSIVO y de grano grueso: la campaña lo toma para TODA su corrida y el loop lo toma
+# por cada ciclo de cola. No se puede afinar más sin romper el `xargs -P2` del loop.
+# `flock` NO es FIFO: como el loop vuelve a pedirlo enseguida, en la práctica la campaña entra en la
+# ventana del IDLE_SLEEP, no entre dos colas (medido, no supuesto). De ahí el techo de espera.
+#
+# OJO, lo que este lock NO cubre: el propio `-P2` del loop puede correr dos recetas que compartan
+# dep, y ésas se pisan igual. El arreglo de fondo es un lock POR ÁRBOL dentro de `fetch`.
+#
+# Espera bloqueante con techo: un ciclo de cola puede ser largo, pero colgarse para siempre sería
+# peor que no correr. Al vencer, sale limpio y lo dice; el próximo lanzamiento reintenta.
+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
+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=""
echo "$(ts) campaña-deuda arranca: $total recetas | go=$(command -v go || echo AUSENTE)" | tee -a "$LOG"
diff --git a/scripts/farm/farm-worker-loop.sh b/scripts/farm/farm-worker-loop.sh
index 4de45367..28ce1fd5 100755
--- a/scripts/farm/farm-worker-loop.sh
+++ b/scripts/farm/farm-worker-loop.sh
@@ -14,6 +14,9 @@ cd "$HAMMER_DIR"
. "$HOME/.cargo/env" 2>/dev/null || true
JOBS="${JOBS:-$(nproc)}"
IDLE_SLEEP="${IDLE_SLEEP:-300}"
+# Fichero de lock compartido con campana-deuda.sh (mismo path en los dos, o no hay exclusión).
+LOCK_FARM="${LOCK_FARM:-$HAMMER_DIR/work/.farm-build.lock}"
+mkdir -p "$HAMMER_DIR/work"
# --- watchdog de disco: cada 3 min borra work/sources/* que ningún bwrap activo bind-monta
# (seguro: sellada=store CAS, en-cola=se re-extrae sola). Evita que el vendoring llene el disco.
@@ -95,7 +98,19 @@ while :; do
[ "$n" -gt 0 ] || continue
total=$((total + n))
echo "$(date -u +%FT%TZ) ciclo: $n recetas en $Q"
- JOBS="$JOBS" PROMOTE=0 QUEUE="$Q" scripts/build-farm.sh 2>&1 | tail -50
+ # LOCK COMPARTIDO CON campana-deuda.sh (2026-07-22): ambos construyen sobre el mismo work/, y
+ # `fetch` nombra el árbol de fuentes de forma determinista (`work/sources/<receta>-<sha16>`) ⇒
+ # dos procesos que construyan la misma DEP apuntan al mismo directorio y uno hace `remove_dir_all`
+ # mientras el otro corre `tar -x`, dejándolo a medias con "Directory not empty" (os error 39) para
+ # siempre. Se toma POR COLA y se suelta al terminarla, en vez de por bucle entero. Más fino no
+ # se puede sin romper el `xargs -P2` de build-farm.sh.
+ # OJO con la expectativa: `flock` NO es FIFO. Como el loop vuelve a pedirlo enseguida, suele
+ # ganarle la carrera a la campaña que espera; medido, la campaña entra al terminar TODAS las
+ # colas, no entre dos. La ventana real es el IDLE_SLEEP. Por eso la campaña espera con techo
+ # (LOCK_WAIT) y sale limpia si no entra, en vez de colgarse.
+ # NO cubre el `-P2` interno: dos recetas de la MISMA cola que compartan dep siguen pudiendo
+ # pisarse. Ese es el arreglo de fondo, un lock por árbol dentro de `fetch`.
+ ( flock 9; JOBS="$JOBS" PROMOTE=0 QUEUE="$Q" scripts/build-farm.sh 2>&1 | tail -50 ) 9>"$LOCK_FARM"
done
if [ "$total" -gt 0 ]; then
echo "$(date -u +%FT%TZ) ciclo terminado; store sellado disponible para el hub"