TAKANA_X con caída a HAMMER_X (ADR 0016). No es un sed: estas variables son
contrato de usuario —knobs del instalador, el entorno del worker, mirror-env.sh—
y viven en perfiles de shell y units FUERA del repo, así que un renombre duro no
falla ruidosamente: la variable no aparece, se toma el default y el build se
comporta distinto sin que nada lo diga.
Rust: takana_core::env::{var,var_os} toma el nombre canónico y deriva el viejo
cambiando el prefijo — se le pasa TAKANA_* para que un grep del nombre nuevo
encuentre todas las lecturas. 5 tests, con DOS controles negativos: sin ninguna
de las dos no hay valor, y un nombre sin prefijo no inventa una caída.
Migrados los 17 sitios directos y los indirectos que el grep no mostraba
(bases_de_mirror, las constantes de kernel_cmd, ROOT_ENV de qorpa, env_path de
recover). takana-recover lleva la caída inline: es un mini-binario que se copia
a /usr/sbin y no vale arrastrarle una dep entera por dos líneas.
Scripts: 30 lecturas pasan a default, manteniendo el
nombre INTERNO de la variable para no tocar sus 190 usos.
Y donde el script EXPORTA en vez de leer, se ponen LAS DOS (mirror-env.sh y el
fragmento in-VM de bootstrap): ahí el lector puede ser un binario viejo —worker
sin recompilar, el /usr/bin/hammer pinado del baseline— que sólo conoce HAMMER_*.
La caída sirve para lectores nuevos; los viejos necesitan que la vieja siga puesta.
Verificado de punta a punta con el binario, no sólo con unit tests: HAMMER_LAB
sigue surtiendo efecto, TAKANA_LAB hace lo mismo, y con las dos gana TAKANA_LAB.
605 tests en verde y el hash de zlib sigue en b3:dc363f26… , intacto.
Cambio de comportamiento que va aparte y hay que decir: el hostname por defecto
de una instalación nueva pasa de 'hammer' a 'takana' (sólo si no se fija ninguna
de las dos variables).
NO se tocan: las rutas /var/lib/hammer de sistemas instalados, el volid
HAMMER_LIVE del ISO, ni el namespace HARKAQ_*, que es de otro subsistema.
158 lines
10 KiB
Bash
Executable File
158 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# store-gc.sh — recolector de basura del store content-addressed. `hammer gc` no existe; esto es.
|
||
#
|
||
# ── EL PROBLEMA QUE RESUELVE ────────────────────────────────────────────────────────────────────
|
||
# El store acumula un artefacto por cada SELLADO, no uno por receta. Cuando una dep cambia, el
|
||
# ArtifactHash de todo lo que cuelga de ella cambia, y el sellado nuevo se suma al viejo en vez de
|
||
# reemplazarlo. Medido el 2026-08-05: 1996 artefactos para 1130 recetas — 12 copias de
|
||
# `libqalculate`, 9 de `qt6-qtdeclarative`, 8 de `gtk4`. 51G de los 74G eran versiones anteriores.
|
||
#
|
||
# ── EL CRITERIO, Y POR QUÉ TIENE DOS ESCALONES ──────────────────────────────────────────────────
|
||
# El conjunto VIVO es `hammer hash` sobre TODAS las recetas de TODAS las colas: es la respuesta a
|
||
# «¿cuál es el hash vigente de esta receta HOY?», que el store solo no puede dar. Lo que no está en
|
||
# ese conjunto es RANCIO — pero rancio se parte en dos cosas muy distintas:
|
||
#
|
||
# SUPERADO — su nombre de receta TIENE un artefacto vigente presente en el store. Es una versión
|
||
# anterior de algo que ya está sellado al día. Borrarlo no pierde nada: si hiciera
|
||
# falta, se reconstruye desde la receta, que está en git.
|
||
#
|
||
# HUÉRFANO — ningún artefacto con ese nombre es el hash vigente. O sea que el hash de HOY no está
|
||
# sellado y éste es el ÚNICO ejemplar que existe. Borrarlo sí pierde: es la diferencia
|
||
# entre «reconstruible en 2 minutos» y «hay que rehacer la torre». En la medición del
|
||
# 2026-08-05 eran 255 artefactos (17G) y casi todos KDE (`kio`×8, `kparts`×8,
|
||
# `kcmutils`×8) — o sea que las recetas KDE se movieron después del último sellado y el
|
||
# escritorio hidratado vive de artefactos que ya no son vigentes.
|
||
#
|
||
# Por eso el default borra SÓLO los superados. `--huerfanos` existe para cuando lo que se quiera sea
|
||
# exactamente eso, y hay que quererlo a propósito.
|
||
#
|
||
# ⚠ EL ESPACIO NO SIEMPRE SE LIBERA. Los rootfs hidratados (`work/*-rootfs`) son HARDLINKS al store.
|
||
# Borrar el directorio del store no rompe el rootfs —los datos siguen vivos por el otro enlace— pero
|
||
# tampoco libera el bloque hasta que el rootfs también se vaya. Es una propiedad, no un fallo.
|
||
#
|
||
# ⚠ NO BARRE `store-rust/` NI `store-kern/`: son stores aparte (frentes selfhost y kernel) con sus
|
||
# propias recetas. Sus `.hammer` son manifiestos de artefacto, no recetas de entrada.
|
||
#
|
||
# Uso: scripts/store-gc.sh [--aplicar] [--huerfanos]
|
||
# (sin --aplicar es un DRY-RUN: mide y no borra)
|
||
# Env: STORE (def ./store) HAMMER (def ./target/release/takana) JOBS (def 8)
|
||
set -uo pipefail
|
||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
|
||
STORE="${STORE:-./store}"; HAMMER="${TAKANA:-${HAMMER:-./target/release/takana}}"; JOBS="${JOBS:-8}"
|
||
APLICAR=0; HUERFANOS=0
|
||
for a in "$@"; do
|
||
case "$a" in
|
||
--aplicar) APLICAR=1 ;;
|
||
--huerfanos) HUERFANOS=1 ;;
|
||
*) echo "opción desconocida: $a" >&2; exit 2 ;;
|
||
esac
|
||
done
|
||
|
||
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
||
|
||
# ── 1. el conjunto VIVO ─────────────────────────────────────────────────────────────────────────
|
||
# Todas las colas, más las recetas puestas en escena en `tandas/`. Las de tandas suelen no resolver
|
||
# sus deps (la resolución es hermano→padre y viven fuera de `recipes/`) y su hash falla; se incluyen
|
||
# igual porque las que SÍ resuelven son vivas de pleno derecho y omitirlas las volvería rancias.
|
||
{ ls recipes/*.toml recipes/incoming-*/*.toml 2>/dev/null
|
||
find tandas -name '*.toml' 2>/dev/null | xargs grep -l '^\[source\]' 2>/dev/null
|
||
} | sort -u > "$TMP/recetas.txt"
|
||
echo "==> recetas barridas: $(wc -l < "$TMP/recetas.txt")"
|
||
|
||
xargs -P "$JOBS" -I{} sh -c "$HAMMER --store $STORE hash \"{}\" 2>/dev/null | tail -1" < "$TMP/recetas.txt" \
|
||
| grep '^b3:' | sed 's/^b3://' | sort -u > "$TMP/vivos.txt"
|
||
echo " hashes vigentes calculables: $(wc -l < "$TMP/vivos.txt")"
|
||
|
||
# Cuántas NO dieron hash. No es fatal (ver arriba) pero es el número que dice cuánta confianza tiene
|
||
# la clasificación: si sube mucho, algo se rompió en la resolución de deps y NO hay que borrar.
|
||
FALLOS=$(( $(wc -l < "$TMP/recetas.txt") - $(
|
||
xargs -P "$JOBS" -I{} sh -c "$HAMMER --store $STORE hash \"{}\" >/dev/null 2>&1 && echo ok" < "$TMP/recetas.txt" | wc -l) ))
|
||
echo " recetas cuyo hash FALLA: $FALLOS"
|
||
|
||
# ── 2. clasificar el store ──────────────────────────────────────────────────────────────────────
|
||
ls "$STORE" | grep -E '^[0-9a-f]{64}-' > "$TMP/dirs.txt"
|
||
python3 - "$TMP" <<'PY'
|
||
import sys, os
|
||
t = sys.argv[1]
|
||
vivos = set(open(f"{t}/vivos.txt").read().split())
|
||
dirs = [l.strip() for l in open(f"{t}/dirs.txt") if l.strip()]
|
||
part = lambda d: (d.split('-', 1)[0], d.split('-', 1)[1])
|
||
nombres_vivos = {n for h, n in map(part, dirs) if h in vivos}
|
||
sup, hue, viv = [], [], []
|
||
for d in dirs:
|
||
h, n = part(d)
|
||
(viv if h in vivos else (sup if n in nombres_vivos else hue)).append(d)
|
||
for f, xs in (("vigentes", viv), ("superados", sup), ("huerfanos", hue)):
|
||
open(f"{t}/{f}.txt", "w").write("".join(x + "\n" for x in xs))
|
||
print(f" artefactos: {len(dirs)} · vigentes {len(viv)} · superados {len(sup)} · huérfanos {len(hue)}")
|
||
PY
|
||
|
||
espacio() { # du sobre una lista de dirs, tolerante a lista vacía
|
||
[ -s "$1" ] || { echo "0"; return; }
|
||
sed "s|^|$STORE/|" "$1" | tr '\n' '\0' | du -sch --files0-from=- 2>/dev/null | tail -1 | cut -f1
|
||
}
|
||
echo "==> espacio: superados $(espacio "$TMP/superados.txt") · huérfanos $(espacio "$TMP/huerfanos.txt")"
|
||
|
||
# ── 3. borrar ───────────────────────────────────────────────────────────────────────────────────
|
||
OBJETIVO="$TMP/superados.txt"
|
||
if [ "$HUERFANOS" = 1 ]; then
|
||
cat "$TMP/superados.txt" "$TMP/huerfanos.txt" > "$TMP/objetivo.txt"; OBJETIVO="$TMP/objetivo.txt"
|
||
echo "!! --huerfanos: se borran TAMBIÉN los únicos ejemplares de su receta"
|
||
fi
|
||
|
||
if [ "$APLICAR" != 1 ]; then
|
||
echo "==> DRY-RUN: no se borró nada. Repetí con --aplicar."
|
||
exit 0
|
||
fi
|
||
|
||
MANIFIESTO="work/store-gc-$(date +%Y-%m-%dT%H%M%S).txt"
|
||
mkdir -p work; cp "$OBJETIVO" "$MANIFIESTO"
|
||
echo "==> manifiesto de lo borrado: $MANIFIESTO"
|
||
ANTES=$(df -h /home 2>/dev/null | tail -1 | awk '{print $4}')
|
||
sed "s|^|$STORE/|" "$OBJETIVO" | xargs rm -rf
|
||
|
||
# ── GUARDIÁN: NO reportar «borrado» sin comprobarlo ─────────────────────────────────────────────
|
||
# 2026-08-07: este script informó «364 artefactos borrados · libres: 24G → 48G» y NO había borrado
|
||
# NINGUNO — los 364 del manifiesto seguían enteros en el store, y los 24G que se liberaron esa vez
|
||
# vinieron de otro lado. `xargs rm -rf` salió con 0 y el `echo` de abajo se lo creyó. (Se reprodujo:
|
||
# corriendo el script en SEGUNDO PLANO el borrado no se materializa; en primer plano sí. Da igual la
|
||
# causa: un `rm` que devuelve 0 no es prueba de que el fichero se fue.)
|
||
#
|
||
# Consecuencia real: la válvula de escape del disco estaba rota EN SILENCIO, y encima reportaba éxito,
|
||
# que es peor que fallar. El disco llegó al 98%.
|
||
#
|
||
# Por eso ahora se RECUENTA sobre el filesystem y el script sale distinto de cero si sobrevivió algo.
|
||
QUEDAN=0
|
||
while IFS= read -r l; do [ -e "$STORE/$l" ] && QUEDAN=$((QUEDAN+1)); done < "$OBJETIVO"
|
||
PEDIDOS=$(wc -l < "$OBJETIVO")
|
||
AHORA=$(df -h /home 2>/dev/null | tail -1 | awk '{print $4}')
|
||
if [ "$QUEDAN" -gt 0 ]; then
|
||
echo "!! BORRADO INCOMPLETO: $QUEDAN de $PEDIDOS artefactos SIGUEN en $STORE."
|
||
echo "!! No te fíes del espacio libre de abajo. Reintentá en PRIMER PLANO y volvé a verificar."
|
||
echo "==> libres: $ANTES → $AHORA"
|
||
exit 1
|
||
fi
|
||
# ── REGISTRO ACUMULADO, para que lo podado NO VUELVA ───────────────────────────────────────────
|
||
# 2026-08-07: se podaron 362 artefactos (24 G) por la mañana y por la tarde volvieron a aparecer LOS
|
||
# MISMOS 362 — solapamiento del 100%. La causa no estaba acá sino en `farm/farm-sync.sh`, que bajaba
|
||
# el store del worker entero; el worker no se poda, así que nos devolvía lo borrado en cada cosecha.
|
||
# Podar → cosechar → vuelven. Este registro es la memoria que le faltaba al sistema: `farm-sync` lo
|
||
# usa como `--exclude-from`. Sin él, el gc es una rueda de hámster que informa éxito cada vez.
|
||
# ⚠ RUTA ABSOLUTA Y `touch` PREVIO. El 2026-08-21 esta línea murió con
|
||
# «work/store-gc-superados.txt: No such file or directory» DESPUÉS de haber borrado ya los 256
|
||
# artefactos — o sea que la poda ocurrió y su registro NO se escribió, que es la combinación exacta
|
||
# que reabre el bucle churn: `farm-sync` no tiene qué excluir y la cosecha los devuelve. El script
|
||
# hace `cd "$ROOT"` al arrancar, así que la ruta relativa DEBERÍA valer; que fallara igual es razón
|
||
# de sobra para no depender de ello en el paso que evita repetir el trabajo de una tanda entera.
|
||
LEDGER="$ROOT/work/store-gc-superados.txt"
|
||
mkdir -p "$ROOT/work" && touch "$LEDGER"
|
||
if cat "$OBJETIVO" "$LEDGER" 2>/dev/null | sort -u > "$LEDGER.tmp" && mv "$LEDGER.tmp" "$LEDGER"; then
|
||
echo "==> registro acumulado: $(wc -l < "$LEDGER") artefactos que NO deben volver ($LEDGER)"
|
||
else
|
||
# Ruidoso a propósito: sin ledger la poda es una rueda de hámster que informa éxito cada vez.
|
||
echo "!! NO pude escribir el registro $LEDGER — los $PEDIDOS borrados VOLVERÁN en la próxima" >&2
|
||
echo "!! cosecha. Reconstruilo con: sort -u $MANIFIESTO > $LEDGER" >&2
|
||
exit 1
|
||
fi
|
||
echo "==> $PEDIDOS artefactos borrados y VERIFICADOS (0 sobrevivientes) · libres: $ANTES → $AHORA"
|