El peso que le faltaba a las ondas: sin él, critical-path = nº de pasos; con él, ETA real en segundos. INSTRUMENTACIÓN (worker): scripts/farm/build-timed.sh envuelve `hammer build` midiendo la pared y la registra en $STORE/.times/<hash>-<host>.json — un SIDECAR del store, NUNCA dentro del artefacto. La duración es no determinista (varía por máquina/carga) ⇒ no puede tocar el ArtifactHash. Keyed por hash (CAS-safe: dos workers no se pisan) + host (varias muestras por receta). Viaja al hub con el rsync de store que ya hace la cosecha; ninguna herramienta del store lo confunde con artefacto. Sólo builds REALES (pared ≥ UMBRAL 3s) — los cache-hits no envenenan la mediana. campana-deuda.sh ahora construye vía build-timed.sh (transparente, mismo exit). CONSUMO (hub): yupana._tiempos() carga name→mediana de segundos; keystones computa el CAMINO CRÍTICO pesado = longest weighted path del subgrafo de deuda (peso = segundos de build). La cadena más larga hay que construirla en SERIE aunque haya ∞ workers ⇒ es la ETA con paralelismo infinito. Degrada con gracia: sin datos, peso=1 y el camino crítico = nº de pasos (lo que ya daban las ondas). Verificado: con muestras sintéticas da "ETA 19 min sobre 5 pasos, kio → kparts → frameworkintegration → breeze → plasma-integration"; sin datos degrada a "5 pasos (SIN datos)". store/.times está git-ignorado (metadata de máquina, no se commitea). LÍMITE honesto (en la cabecera de build-timed.sh): `hammer build` arrastra deps ⇒ la pared incluye deps no selladas. En orden topológico (drenar) las deps ya están selladas y la pared mide sobre todo ESTA receta — cota superior buena para pesar. La precisión exacta pediría instrumentar el sellado dentro de hammer-build (Rust). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
5.6 KiB
Bash
Executable File
96 lines
5.6 KiB
Bash
Executable File
#!/bin/sh
|
|
# campana-deuda.sh — muele en el WORKER una lista EXPLÍCITA de recetas en deuda, medida en el HUB.
|
|
#
|
|
# POR QUÉ NO `saldar-deuda-static.sh` DIRECTO EN EL WORKER (2026-07-21): ese script calcula la deuda
|
|
# en vivo con `hammer hash --check` contra el store LOCAL. En el worker el store es PARCIAL (el del
|
|
# volumen: ~290 artefactos vs 720 del hub) ⇒ mide 716 recetas de deuda en vez de 37 y se pone a
|
|
# reconstruir media distro. Es la regla de [[frente-harkaq-jaula]] con otra cara: **el worker MIDE,
|
|
# el hub CLASIFICA**. Acá el hub decide la lista (`DRY=1 saldar-deuda-static.sh`) y el worker sólo
|
|
# ejecuta. La lista se pasa por argumentos o por DEUDA=, no se recalcula.
|
|
#
|
|
# ORDEN: las RAÍCES del stack GUI primero (glib unblocks=14, luego harfbuzz/cairo/pango → gtk4 →
|
|
# libadwaita). `hammer build` arrastra deps recursivamente, así que el orden no es obligatorio —
|
|
# pero empezar por la raíz hace que la cascada caiga cuanto antes y que un corte temprano deje el
|
|
# máximo destrabado. El stack GUI es lo que el laptop NO puede construir (zig-skew, ver
|
|
# [[etapa-g-gui-chain-boundary]]) ⇒ es exactamente lo que justifica la granja.
|
|
#
|
|
# PATH: `go mod vendor` y demás corren HOST-SIDE (fuera del sandbox) ⇒ el `go` del store tiene que
|
|
# estar en el PATH. Una sesión SSH no interactiva no carga /etc/profile ni cargo/env: sin esto toda
|
|
# receta Go muere con "spawn go mod vendor: No such file or directory".
|
|
#
|
|
# Uso (en el worker): scripts/farm/campana-deuda.sh <receta> [receta…]
|
|
# DEUDA="glib cairo" scripts/farm/campana-deuda.sh
|
|
# Lanzarlo desde el hub para que sobreviva al cierre del SSH:
|
|
# ssh root@<ip> 'systemd-run --unit=campana-deuda --working-directory=/opt/hammer \
|
|
# /opt/hammer/scripts/farm/campana-deuda.sh <recetas…>'
|
|
# ssh root@<ip> 'journalctl -u campana-deuda -f'
|
|
set -u
|
|
HAMMER_DIR="${HAMMER_DIR:-/opt/hammer}"
|
|
cd "$HAMMER_DIR"
|
|
HAMMER="${HAMMER:-$HAMMER_DIR/target/release/hammer}"
|
|
STORE="${STORE:-$HAMMER_DIR/store}"
|
|
LOG="${LOG:-/var/log/campana-deuda.log}"
|
|
|
|
# go del store (host-side) + cargo, para que las recetas Go/Rust no mueran por PATH
|
|
for g in "$STORE"/*-go/usr/bin; do [ -d "$g" ] && PATH="$g:$PATH"; done
|
|
HOME="${HOME:-/root}"; export HOME # systemd-run no hereda HOME y `set -u` lo convierte en fatal
|
|
[ -r "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
|
|
export PATH
|
|
|
|
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"
|
|
|
|
for r in $DEUDA; do
|
|
i=$((i + 1))
|
|
f="recipes/$r.toml"
|
|
[ -r "$f" ] || f="$(ls recipes/incoming*/"$r".toml 2>/dev/null | head -1)"
|
|
[ -n "$f" ] && [ -r "$f" ] || { echo "$(ts) [$i/$total] $r ⚠ sin receta" | tee -a "$LOG"; continue; }
|
|
echo "$(ts) [$i/$total] → $r" | tee -a "$LOG"
|
|
# heartbeat: el dead-man ya cuenta `hammer build` como trabajo, pero entre receta y receta hay
|
|
# huecos (fetch/vendor) donde no hay proceso con ese nombre. Tocarlo evita una muerte espuria.
|
|
touch /run/hammer-heartbeat 2>/dev/null || true
|
|
# build-timed.sh mide la pared y registra la duración en $STORE/.times (sólo builds reales), para
|
|
# el camino crítico pesado de yupana. Transparente: sale con el mismo código que `hammer build`.
|
|
if HAMMER="$HAMMER" STORE="$STORE" "$HAMMER_DIR/scripts/farm/build-timed.sh" "$f" >>"$LOG" 2>&1; then
|
|
ok=$((ok + 1)); echo "$(ts) [$i/$total] ✓ $r" | tee -a "$LOG"
|
|
else
|
|
fail=$((fail + 1)); falladas="$falladas $r"; echo "$(ts) [$i/$total] ✗ $r" | tee -a "$LOG"
|
|
fi
|
|
done
|
|
|
|
echo "$(ts) campaña-deuda fin: $ok selladas, $fail fallidas de $total" | tee -a "$LOG"
|
|
[ -n "$falladas" ] && echo "$(ts) fallidas:$falladas" | tee -a "$LOG"
|
|
exit 0
|