Files
hammer/scripts/store-gc.sh
T
sergioandClaude Opus 5 514ae44135 store-gc: guardián — no reportar «borrado» sin comprobarlo
El script informó «364 artefactos borrados · libres: 24G → 48G» y no había
borrado ninguno: los 364 de su propio manifiesto seguían enteros en el store.
`xargs rm -rf` salió con 0 y el echo se lo creyó. Se reprodujo: corriéndolo 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.
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% mientras yo creía haber
liberado 24G.

Ahora recuenta sobre el filesystem y sale distinto de cero si sobrevivió algo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 07:44:54 -04:00

136 lines
8.3 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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/hammer) JOBS (def 8)
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT"
STORE="${STORE:-./store}"; HAMMER="${HAMMER:-./target/release/hammer}"; 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
echo "==> $PEDIDOS artefactos borrados y VERIFICADOS (0 sobrevivientes) · libres: $ANTES$AHORA"