From 1b436a717f596fef7c813f3112aff05ce1dcf856 Mon Sep 17 00:00:00 2001 From: sergio Date: Wed, 22 Jul 2026 17:06:42 -0400 Subject: [PATCH] farm: lock compartido entre campana-deuda y el worker-loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Los dos construyen sobre el MISMO work/, y `fetch` nombra el árbol de fuentes de forma determinista (`work/sources/-`, 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) --- scripts/farm/campana-deuda.sh | 28 ++++++++++++++++++++++++++++ scripts/farm/farm-worker-loop.sh | 17 ++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) 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…] (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/-`) ⇒ 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/-`) ⇒ + # 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"