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 1346Shell 97Rust 63Markdown 571650 Sutichaykuna

⎇ 14bed923ad1cfc03957cb3bddc9e4229e6b0ec9f scripts/farm/farm-worker-loop.sh Shell · 147 siq'ikuna · 10.1 KB Ñawpa willay ch'uya

granja: el worker no molía GNOME — faltaban dos colas en QUEUES 14bed923a · sergio ·

1#!/usr/bin/env bash
2# farm-worker-loop.sh — loop AUTÓNOMO del worker. Construye la cola recipes/incoming/ en
3# bucle (PROMOTE=0: sólo sella al store local, NO promueve, NO firma, NO toca gitea), con
4# watchdog de disco integrado. El hub (laptop) le mete recetas nuevas vía farm-sync.sh y le
5# baja el store sellado; este loop sólo muele lo que haya, 24/7, sin esperar a nadie.
6#
7# Idempotente: cada ciclo re-escanea incoming/; lo ya-sellado sale por cache-hit (instantáneo),
8# sólo construye lo nuevo. Cuando la cola está toda construida, duerme IDLE_SLEEP y reintenta.
9#
10# Lo lanza systemd (hammer-farm.service). Manual:  JOBS=2 ./scripts/farm/farm-worker-loop.sh
11set -uo pipefail
12HAMMER_DIR="${HAMMER_DIR:-/opt/hammer}"
13cd "$HAMMER_DIR"
14. "$HOME/.cargo/env" 2>/dev/null || true
15# JOBS=1 MIENTRAS EL ADR 0012 ESTÉ SIN DECIDIR (2026-07-22). `build-farm.sh` usa `xargs -P$JOBS`, y
16# con >1 dos recetas que compartan dep disparan la carrera del árbol de fuentes: ambas hacen fetch
17# del mismo `work/sources/<dep>-<sha>`, una borra mientras la otra usa, y el árbol queda ROTO PARA
18# SIEMPRE. Se vio a escala: al invalidar libdrm, 118 de las 205 recetas KDE tuvieron que reconstruir
19# de verdad (antes eran cache-hits que ocultaban el problema) y ~93 murieron con
20# "/src/.zwrap/cc is not a full path to an existing compiler tool" — el wrapper de zig que la receta
21# deja EN EL ÁRBOL DE FUENTE, barrido por el fetch concurrente de otra receta sobre el mismo qtbase.
22# El reintento serial de build-farm tampoco las salva, porque el árbol ya quedó inconsistente.
23# Serializar es la única mitigación correcta hasta que el ADR 0012 elija (lock por árbol / árbol
24# privado / caché inmutable + copia). Subirlo otra vez sin resolver eso reintroduce el bug.
25JOBS="${JOBS:-1}"
26IDLE_SLEEP="${IDLE_SLEEP:-300}"
27# Fichero de lock compartido con campana-deuda.sh (mismo path en los dos, o no hay exclusión).
28LOCK_FARM="${LOCK_FARM:-$HAMMER_DIR/work/.farm-build.lock}"
29mkdir -p "$HAMMER_DIR/work"
30
31# --- watchdog de disco: cada 3 min borra work/sources/* que ningún bwrap activo bind-monta
32# (seguro: sellada=store CAS, en-cola=se re-extrae sola). Evita que el vendoring llene el disco.
33# Además, si el disco supera DISK_HIGH%, purga los CACHES Go (GOMODCACHE ~/go/pkg/mod + gocache):
34# el `go mod vendor` de las recetas Go acumula ahí decenas de GB sin tope (el vendor/ local de cada
35# receta ya tiene lo que el build necesita; el modcache sólo se usa DURANTE el vendor). Si la purga
36# pisa un vendor en curso, esa receta reintenta el próximo ciclo (cache-hit el resto). Sin esto, una
37# tanda Go grande llena el disco de 80G y el I/O-wait dispara el load (cuello real, no CPU/RAM).
38DISK_HIGH="${DISK_HIGH:-82}"
39(
40  while :; do
41    sleep 180
42    # Protección 1: árboles que un bwrap activo bind-monta (fase compile dentro del sandbox).
43    active=$(pgrep -af bwrap 2>/dev/null | grep -oE 'work/sources/[^ ]+' | sort -u)
44    # Protección 2: árboles cuya receta tiene un `hammer build` EN VUELO. Durante el fetch/`go mod
45    # vendor`/resolve_phases (host-side, ANTES de que arranque el bwrap) el árbol no está bind-montado
46    # ⇒ la protección 1 no lo cubre. Una tanda Go con árbol de deps enorme (dnscontrol/lazysql) tarda
47    # minutos vendoreando; sin esto, el watchdog le borra el go.mod a mitad → "no build system".
48    building=$(pgrep -af 'release/hammer' 2>/dev/null | grep -oE '[^ ]+\.toml' | sed 's#.*/##;s#\.toml$##' | sort -u)
49    for d in work/sources/*/; do
50      [ -d "$d" ] || continue; dd=${d%/}
51      printf '%s\n' "$active" | grep -qxF "$dd" && continue
52      nm=$(basename "$dd"); nm=${nm%-*}   # work/sources/<name>-<sha40> → <name> (el sha no trae '-')
53      printf '%s\n' "$building" | grep -qxF "$nm" && continue
54      # Protección 3: árbol CALIENTE (modificado hace <2 min) ⇒ se está extrayendo AHORA. Una dep
55      # transitiva (p.ej. cairo bajo pango/gtk4) se extrae host-side ANTES del bwrap y con un nombre
56      # que NO coincide con la receta en vuelo ⇒ protecciones 1 y 2 no la cubren. Sin esto, el rm -rf
57      # compite con el tar y deja el árbol a medias ("Cannot mkdir" / luego "Directory not empty" para
58      # siempre). Un árbol en frío (extracción ya fallida/terminada) queda purgable ⇒ se re-extrae limpio.
59      [ -n "$(find "$dd" -mmin -2 -print -quit 2>/dev/null)" ] && continue
60      rm -rf "$dd"
61    done
62    use=$(df --output=pcent "$HAMMER_DIR" 2>/dev/null | tr -dc '0-9')
63    if [ -n "$use" ] && [ "$use" -ge "$DISK_HIGH" ]; then
64      # OJO: `go clean -modcache`/`-cache` borran ~/go/pkg/mod y ~/.cache/go-build ENTEROS. Ambos los
65      # usa `go mod vendor` (modcache p/ módulos, go-build p/ verificar el grafo). Si la purga corre
66      # mientras un vendor está activo —o en la VENTANA justo antes de que arranque (otra receta de la
67      # cola en serie)— le arranca el cache bajo los pies (`.partial`/`go-build/...: no such file`) y
68      # ESE vendor falla. La purga de work/sources de arriba ya liberó lo grueso (segura: saltea las
69      # protegidas). Por eso NO purgo las caches Go si hay CUALQUIER `hammer build` en vuelo (no solo
70      # un `go mod vendor` visible en este instante): difiero al próximo tick / al gap idle entre
71      # ciclos. El sandbox `go install` usa GOCACHE=/src + -mod=vendor ⇒ no toca estas caches del host.
72      if pgrep -f 'go mod vendor' >/dev/null 2>&1 || pgrep -f 'release/hammer.* build ' >/dev/null 2>&1; then
73        echo "$(date -u +%FT%TZ) watchdog: disco ${use}% ≥ ${DISK_HIGH}% pero hay build en vuelo ⇒ difiero purga caches Go"
74      else
75        echo "$(date -u +%FT%TZ) watchdog: disco ${use}% ≥ ${DISK_HIGH}% ⇒ purgo caches Go"
76        go clean -modcache 2>/dev/null; go clean -cache 2>/dev/null
77      fi
78    fi
79  done
80) &
81
82# Colas a construir (sólo SELLA, PROMOTE=0; el hub promueve+firma). `recipes/incoming` es la cola de
83# staging general; `recipes/incoming-go` es la AISLADA del frente Go (pre-sembrada para moler 24/7).
84# build-farm hace `rm -rf work/farm` al inicio ⇒ construirlas EN SERIE (una por vuelta) es seguro,
85# sin colisión. El glob es TOP-LEVEL (`$Q/*.toml`), así que `recipes/incoming/.deferred/` (24 recetas
86# aparcadas con diagnóstico, p.ej. git con muro en libgit.a) NO se muele — es un subdir a propósito.
87# incoming-clib: libs C (toolkit/foundational) que el worker SELLA pero el cron de cosecha-go IGNORA
88# (son build-deps, se usan vía catálogo; no tienen binario que el gate Go pueda smoke-testear).
89#
90# ⚠ EL ORDEN IMPORTA Y LAS COLAS GNOME VAN ANTES DE KDE (2026-08-06). Este default se escribió cuando
91# `incoming-gnome-onda1` era el frente GNOME entero, y quedó fósil: faltaban `incoming-gnome` (86
92# recetas, donde viven gtk4/mutter/gnome-shell) y `incoming-gnome-onda2` (la isla dinámica). O sea que
93# el worker decía moler GNOME y molía 6 recetas de 114. Además iban DESPUÉS de las 205 de KDE, así que
94# aunque hubieran estado listadas, un ciclo con la cola KDE llena las dejaba sin turno.
95QUEUES="${QUEUES:-recipes/incoming recipes/incoming-go recipes/incoming-clib recipes/incoming-gnome-onda1 recipes/incoming-gnome-onda2 recipes/incoming-gnome recipes/incoming-kde}"
96
97# REBUILD DEL BINARIO AL ARRANCAR (2026-07-18): el hammer horneado en la imagen golden ENVEJECE.
98# Caso real que costó una noche de KDE: la golden del 29-jun traía un hammer sin el fix del overlay
99# lowerdir (commit e69eaaa, 12-jul) ⇒ toda receta de muchas deps (kio=52, kwin, plasma-*) desbordaba
100# el límite de 4KB del mount options y el sandbox NI ARRANCABA — 43 recetas atascadas jurando "en
101# cola". El source SÍ llega fresco (farm-sync rsync-ea el repo), pero nadie recompilaba. Reconstruir
102# aquí garantiza que el worker corre el CÓDIGO VIGENTE, no el fósil de la imagen. Cacheado: ~24s si
103# no cambió nada, minutos en frío. Si cargo falla, seguimos con el binario que haya (mejor que abortar).
104if command -v cargo >/dev/null 2>&1; then
105  echo "$(date -u +%FT%TZ) rebuild hammer desde el source rsync-eado (evita el fósil de la golden)…"
106  cargo build --release --bin hammer -j"$JOBS" 2>&1 | tail -2 || echo "  ⚠ rebuild falló — uso el binario existente"
107fi
108
109echo "$(date -u +%FT%TZ) worker-loop arrancado: JOBS=$JOBS IDLE_SLEEP=$IDLE_SLEEP QUEUES='$QUEUES'"
110while :; do
111  total=0
112  for Q in $QUEUES; do
113    n=$(ls "$Q"/*.toml 2>/dev/null | wc -l)
114    [ "$n" -gt 0 ] || continue
115    total=$((total + n))
116    echo "$(date -u +%FT%TZ) ciclo: $n recetas en $Q"
117    # LOCK COMPARTIDO CON campana-deuda.sh (2026-07-22): ambos construyen sobre el mismo work/, y
118    # `fetch` nombra el árbol de fuentes de forma determinista (`work/sources/<receta>-<sha16>`) ⇒
119    # dos procesos que construyan la misma DEP apuntan al mismo directorio y uno hace `remove_dir_all`
120    # mientras el otro corre `tar -x`, dejándolo a medias con "Directory not empty" (os error 39) para
121    # siempre. Se toma POR COLA y se suelta al terminarla, en vez de por bucle entero. Más fino no
122    # se puede sin romper el `xargs -P2` de build-farm.sh.
123    # OJO con la expectativa: `flock` NO es FIFO. Como el loop vuelve a pedirlo enseguida, suele
124    # ganarle la carrera a la campaña que espera; medido, la campaña entra al terminar TODAS las
125    # colas, no entre dos. La ventana real es el IDLE_SLEEP. Por eso la campaña espera con techo
126    # (LOCK_WAIT) y sale limpia si no entra, en vez de colgarse.
127    # NO cubre el `-P2` interno: dos recetas de la MISMA cola que compartan dep siguen pudiendo
128    # pisarse. Ese es el arreglo de fondo, un lock por árbol dentro de `fetch`.
129    # El `flock -n` primero es sólo para poder DECIR que estamos esperando: si el log dijera
130    # "ciclo: N recetas" y después nada, parecería que el loop trabaja cuando en realidad está
131    # bloqueado, y ese malentendido es exactamente el que cuesta horas de diagnóstico.
132    (
133      if ! flock -n 9; then
134        echo "$(date -u +%FT%TZ) esperando el lock de build (campana-deuda lo tiene)…"
135        flock 9
136        echo "$(date -u +%FT%TZ) lock tomado, sigo con $Q"
137      fi
138      JOBS="$JOBS" PROMOTE=0 QUEUE="$Q" scripts/build-farm.sh 2>&1 | tail -50
139    ) 9>"$LOCK_FARM"
140  done
141  if [ "$total" -gt 0 ]; then
142    echo "$(date -u +%FT%TZ) ciclo terminado; store sellado disponible para el hub"
143  else
144    echo "$(date -u +%FT%TZ) colas vacías; durmiendo ${IDLE_SLEEP}s"
145  fi
146  sleep "$IDLE_SLEEP"
147done