From 33e02bdfbc63f0b2c3472c75420a017439a6abc1 Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 31 Aug 2026 19:56:42 +0000 Subject: [PATCH] poda de fuentes: 70 G recuperados y un vigia para que no vuelvan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El volumen del store estaba al 97% (8,8 G libres) con la granja escribiendo ahi, y el gordo NO era el store: eran 79 G de `work/sources` — 106 arboles con el `target/` de cargo y los `.o` dentro, porque el arbol de fuentes es tambien el directorio de compilacion. `pixi` pesaba 3,5 G y siete apps cosmic pasaban de 3 G cada una. Nadie podaba eso: store-gc mira el store, la poda de CI mira la cache de CI, y este tercer monton crecia sin dueno desde que el store se mudo al volumen. Borrarlo no cuesta nada, y eso es lo que lo hace seguro: los dos caminos de fetch.rs (fetch_git:73 y fetch_tarball:234) hacen `remove_dir_all` del arbol y lo re-extraen en CADA build, sin una sola rama que reutilice. Un arbol viejo no acelera nada; la re-extraccion ocurre igual. Lo unico que se pierde es el post-mortem del ultimo build, de ahi el suelo de 24 h. El script toma `work/.farm-build.lock` porque el unico dano posible es borrar el arbol que un bwrap esta compilando (ADR 0012 en su forma mas directa), y el mtime del directorio raiz no distingue "viejo" de "build lento". Con espera acotada: si hay algo en vuelo salta el ciclo. Suelo en HORAS y no en dias por la leccion de cache-ci-no-envejece: aquel cron podaba a +10 dias sobre datos de horas y liberaba cero. Los 106 arboles abarcaban 3 dias. Sin umbral por espacio libre: podar solo cerca del borde convierte una poda barata y plana en un pico justo cuando un build puede quedarse sin disco a mitad. Probado en las cuatro ramas: dry-run vacio, dry-run con 15 candidatos, --aplicar sobre un arbol sintetico (borra el viejo, deja los 15 recientes) y lock tomado (salta y sale 0). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih --- scripts/farm/cosecha-cron.sh | 25 +++++++++ scripts/poda-fuentes.sh | 104 +++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100755 scripts/poda-fuentes.sh diff --git a/scripts/farm/cosecha-cron.sh b/scripts/farm/cosecha-cron.sh index dbb34c4b..c3b0f935 100755 --- a/scripts/farm/cosecha-cron.sh +++ b/scripts/farm/cosecha-cron.sh @@ -151,6 +151,31 @@ if [ -s "$FLEET" ] && command -v hcloud >/dev/null 2>&1; then printf '%s' "$sobreviven" | grep -v '^[[:space:]]*$' > "$FLEET.tmp" 2>/dev/null; mv "$FLEET.tmp" "$FLEET" 2>/dev/null || true fi +# 2.7 PODA DE `work/sources` (2026-08-31). Cuarto vigía, y el único que no emite veredicto: su +# salida es que el disco se quede plano. +# +# POR QUÉ EXISTE: ese día el volumen del store (`/dev/sdb`, 246 G) estaba al **97%, 8,8 G libres**, +# y el gordo NO era el store — eran **79 G de `work/sources`**, 106 árboles con el `target/` de +# cargo y los `.o` dentro (el árbol de fuentes es también el directorio de compilación). Nada en la +# granja podaba eso: `store-gc.sh` mira el store, la poda de caché de CI mira la caché de CI, y este +# tercer montón crecía sin dueño desde que el store se mudó al volumen. Se recuperaron 70 G. +# +# POR QUÉ ES SEGURO: no es una caché. Los dos caminos de `hammer-build/src/fetch.rs` borran el árbol +# y lo re-extraen en CADA build, sin una sola rama que reutilice ⇒ un árbol viejo no acelera nada y +# borrarlo no cuesta ni la re-extracción. Lo único que se pierde es el post-mortem del último build +# de esa receta, y por eso el suelo son 24 h y no cero. +# +# CADA CICLO, no con puerta diaria como el audit: el `find` sobre ~100 directorios es instantáneo y +# el `rm` cuesta en proporción a la basura que haya. Con puerta diaria la basura de un día entero de +# granja llegaría junta, que es justo lo que se quiere evitar. +# +# El script toma `work/.farm-build.lock` por dentro y con espera acotada: borrar el árbol que un +# `bwrap` está compilando es el ADR 0012 en su forma más directa, y el mtime no distingue "viejo" de +# "build lento". Si hay algo en vuelo, salta el ciclo y sale 0. +if [ -x "$ROOT/scripts/poda-fuentes.sh" ]; then + "$ROOT/scripts/poda-fuentes.sh" --aplicar || echo " ⚠ poda de fuentes falló (rc=$?) — sigo" +fi + # 3. Regenerar el grafo de estado desde el store ya cosechado (usa el hammer del laptop, que tiene # el subcomando `hash`). Barato (~14s cada uno). El corpus canónico y el frente KDE, por separado. echo "==> regenerando grafo de estado" diff --git a/scripts/poda-fuentes.sh b/scripts/poda-fuentes.sh new file mode 100755 index 00000000..8e992fbf --- /dev/null +++ b/scripts/poda-fuentes.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# poda-fuentes.sh — recolector de `work/sources/`. Hermano de `store-gc.sh`, pero el criterio no +# tiene nada que ver, y confundirlos sería caro. +# +# ── QUÉ SE ESTABA ACUMULANDO ──────────────────────────────────────────────────────────────────── +# Medido el 2026-08-31 en gioser: `work/sources` pesaba **79 G** — 106 árboles, media de 745 MB, +# los peores `pixi` 3,5 G y siete apps cosmic por encima de 3 G cada una. El volumen del store +# (`/dev/sdb`, 246 G) estaba al **97%, 8,8 G libres**, con la granja escribiendo ahí. +# +# Y esos 3,5 G no son fuente: el árbol de `work/sources/-` es a la vez el sitio donde +# se extrae Y donde se compila (hammer parchea y vendorea in situ). Lo que engorda es el `target/` +# de cargo, los `.o`, el `vmlinux`. Es RESIDUO DE BUILD, no código. +# +# ── POR QUÉ BORRARLO NO CUESTA NADA (esto es lo que hace segura la poda) ──────────────────────── +# No es una caché. `hammer-build/src/fetch.rs` tiene exactamente dos caminos —`fetch_git` (línea +# 73) y `fetch_tarball` (línea 234)— y **los dos hacen `remove_dir_all` del árbol y lo re-extraen, +# incondicionalmente, en cada build**. No hay ni una rama que reutilice un árbol existente. +# +# ⇒ Borrar un árbol no cuesta ni la re-extracción: ésa ocurre igual la próxima vez que se construya +# esa receta. Lo ÚNICO que se pierde es poder mirar por dentro el último build de esa receta +# —el post-mortem de uno que falló—, y ese valor se muere en un día. +# +# ── POR QUÉ TOMA `work/.farm-build.lock` (y no es opcional) ───────────────────────────────────── +# El único modo de que esto haga daño es borrar el árbol que un `bwrap` está usando para compilar: +# el ADR 0012 en su forma más directa, y el mismo desastre medido en CLAUDE.md §1 (93 de 205 +# recetas KDE muertas con `cc is not a full path`). El lock es el punto de serialización que ya +# usan `farm-worker-loop.sh` y `campana-deuda.sh`, así que tomarlo acá nos serializa con la granja. +# +# ⚠ Y hace falta DE VERDAD, no por prudencia: el filtro es por mtime del directorio raíz, que sólo +# cambia cuando se añaden o quitan entradas de primer nivel. Un build largo en vuelo puede tener +# un raíz con mtime de hace dos días mientras compila abajo. El mtime NO distingue "viejo" de +# "lento". El lock sí. +# +# ── POR QUÉ EL SUELO ES EN HORAS Y NO EN DÍAS ─────────────────────────────────────────────────── +# La lección de `cache-ci-no-envejece`: aquel cron podaba a +10 días sobre datos que se reescriben +# enteros cada ráfaga, o sea que liberaba CERO y parecía que protegía. **Un guardián calibrado a +# una escala que el dato nunca alcanza no protege.** Acá los 106 árboles abarcaban 3 días (76 de +# uno solo) ⇒ un umbral en días grandes no habría tocado nada. 24 h deja el post-mortem del día y +# se lleva el resto. +# +# ── POR QUÉ NO SE CONDICIONA A ESPACIO LIBRE ──────────────────────────────────────────────────── +# Porque borrar no cuesta nada. Un umbral por espacio ("podar sólo bajo 20 G") deja que la basura +# se acumule hasta rozar el borde y convierte una poda barata y plana en un pico cerca del límite, +# que es justo cuando un build en vuelo puede quedarse sin disco a mitad. Se mantiene plano. +# +# Uso: scripts/poda-fuentes.sh [--aplicar] [--horas N] +# (sin --aplicar es DRY-RUN: mide y no borra) +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" +FUENTES="${FUENTES:-work/sources}" +HORAS="${HORAS:-24}"; APLICAR=0; ESPERA="${ESPERA:-60}" +while [ $# -gt 0 ]; do + case "$1" in + --aplicar) APLICAR=1 ;; + --horas) HORAS="$2"; shift ;; + *) echo "opción desconocida: $1" >&2; exit 2 ;; + esac + shift +done + +[ -d "$FUENTES" ] || { echo "no existe $FUENTES — nada que podar"; exit 0; } + +gigas() { echo "$1" | awk '{printf "%.1f", $1/1073741824}'; } + +# `-mmin +N` en vez de `-mtime`: `-mtime +0` es ">24h" por redondeo hacia abajo y no deja expresar +# nada más fino. Con minutos el umbral se dice tal cual. +MINUTOS=$((HORAS * 60)) + +poda() { + local libre_antes libre_despues n + libre_antes=$(df -B1 --output=avail "$FUENTES" | tail -1) + mapfile -t viejos < <(find "$FUENTES" -mindepth 1 -maxdepth 1 -type d -mmin "+$MINUTOS" | sort) + n=${#viejos[@]} + local quedan + quedan=$(find "$FUENTES" -mindepth 1 -maxdepth 1 -type d -not -mmin "+$MINUTOS" | wc -l) + + echo "── poda de $FUENTES · suelo ${HORAS}h · libres $(gigas "$libre_antes") G" + echo " $n árbol(es) por encima del suelo · $quedan se quedan (post-mortem del día)" + [ "$n" -eq 0 ] && { echo " nada que podar"; return 0; } + + if [ "$APLICAR" -eq 0 ]; then + # El dry-run NO mide con `du`: recorrer 79 G de árboles cuesta minutos y este script corre + # dentro del lock. Se nombran los candidatos y se deja el tamaño para el --aplicar, que lo saca + # gratis del `df` de antes y después. + printf ' · %s\n' "${viejos[@]}" | head -20 + [ "$n" -gt 20 ] && echo " … y $((n - 20)) más" + echo " DRY-RUN: no se borró nada (usar --aplicar)" + return 0 + fi + + printf '%s\0' "${viejos[@]}" | xargs -0 -r rm -rf + libre_despues=$(df -B1 --output=avail "$FUENTES" | tail -1) + echo " borrados $n · libres $(gigas "$libre_despues") G · LIBERADO $(gigas $((libre_despues - libre_antes))) G" +} + +# `-w`: si hay un build en vuelo NO se espera indefinidamente. Este script lo llama el cron cada 30 +# min; quedarse colgado del lock durante un kernel de 2 h dejaría al cron sin cosechar. Saltar un +# ciclo no cuesta nada porque la basura no se va a ningún lado. +exec 9>work/.farm-build.lock +if ! flock -w "$ESPERA" 9; then + echo "── poda de $FUENTES: build en vuelo (lock tomado tras ${ESPERA}s) ⇒ este ciclo NO toca" + exit 0 +fi +poda