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>
This commit is contained in:
2026-07-22 17:06:42 -04:00
co-authored by Claude Opus 4.8
parent 0639d8a53d
commit 1b436a717f
2 changed files with 44 additions and 1 deletions
+28
View File
@@ -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"
+16 -1
View File
@@ -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"