tawasuyu

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 1574Shell 204Rust 111Markdown 993763 Definiciones

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>

1b436a717f596fef7c813f3112aff05ce1dcf856 · por sergio · (hace 3 meses) · padre 0639d8a53 · ver el árbol en este commit

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"