store en el volumen: «está sellado» dejó de ser «está en este disco»
El store de trabajo se mudó al volumen de la granja y el laptop quedó con 219 artefactos: los de fuente privada (que la granja no puede rehacer) más lo que todavía no está respaldado. 979 artefactos verificados en el box se borraron de acá, y con ellos el caché .dmerge de 92 G que sostenía sus bloques: 128 G → 8,2 G, 115 G libres. Eso rompía tres cosas que leían el disco local como si fuera la verdad: - build-state.py habría reportado `never` sobre ~950 sellados y el latido lo habría COMMITEADO. Un grafo recién escrito miente con más autoridad que uno viejo. Ahora resuelve la presencia contra la unión del store y los manifiestos, y expone `sealed_remoto` para que «el store se mudó» no se lea nunca como «el corpus creció». - farm-sync.sh armaba el manifiesto con `ls ./store`, así que le habría dicho al worker «el hub tiene 220» y el worker habría rehecho ~950. Es el bucle de churn que el propio fichero documenta, al revés. Ahora es la unión, con un guardián que aborta si el manifiesto encoge. - cosecha-cron.sh bajaba el store entero cada 30 min: habría deshecho la mudanza sola, como la poda de 24 G que se deshacía en 2026-08-07. Ahora baja sólo la lista de nombres. Los cuatro grafos regenerados dan idéntico a antes del recorte (766/11/2), que es la prueba de que no se perdió nada. Queda abierto: los artefactos nuevos viven SÓLO en el volumen hasta que alguien corra una pasada de respaldo. El volumen tiene borrado protegido, pero es una copia. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+48
-3
@@ -139,13 +139,54 @@ def vigente_hash(path):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# ── El store dejó de vivir en el laptop (2026-08-09) ───────────────────────────────────────────
|
||||
# El store de trabajo se mudó al VOLUMEN de la granja y el disco local se quedó sólo con lo que la
|
||||
# granja NO puede rehacer (las recetas de fuente privada) más lo que aún no está respaldado. Si el
|
||||
# grafo siguiera leyendo únicamente `./store`, el latido regeneraría un estado que dice `never` sobre
|
||||
# 979 artefactos que existen y están sellados — y lo COMMITEARÍA, que es peor que no regenerarlo:
|
||||
# un grafo viejo miente con la misma cara que uno fresco, pero uno recién escrito miente con más
|
||||
# autoridad. El error no sería del disco, sería de haber confundido «dónde están los bytes» con
|
||||
# «qué está construido».
|
||||
#
|
||||
# Así que la presencia se resuelve contra la UNIÓN de dos fuentes: el store local y el MANIFIESTO de
|
||||
# sellados que la granja deja en `work/farm-sellados.txt`. La unión es monótona a propósito — sólo
|
||||
# puede evitar un `never`/`debt` falso, nunca inventar uno. El riesgo simétrico (dar por sellado algo
|
||||
# que ya no está en ningún lado) queda VISIBLE en los totales como `sealed_remoto`, en vez de
|
||||
# disolverse en el número grande.
|
||||
# Dos registros, y hacen falta LOS DOS: el de la granja se escribe al cosechar y va por delante,
|
||||
# pero el 2026-08-09 le faltaban 5 sellados que el respaldo sí tenía ⇒ solo con él, 5 recetas caían
|
||||
# de `sealed` a `debt` sin que nada se hubiera roto. El del respaldo se refresca con
|
||||
# `scripts/respaldo-storagebox.sh listar` y es el más completo, pero llega por red y envejece.
|
||||
MANIFIESTOS = ("farm-sellados.txt", "respaldo-sellados.txt")
|
||||
|
||||
def _manifiesto_sellados():
|
||||
out = set()
|
||||
for nombre in MANIFIESTOS:
|
||||
try:
|
||||
txt = (ROOT / "work" / nombre).read_text()
|
||||
except OSError:
|
||||
continue
|
||||
out |= {l.strip() for l in txt.splitlines() if l.strip() and not l.startswith("#")}
|
||||
return out
|
||||
|
||||
SELLADOS_REMOTOS = _manifiesto_sellados()
|
||||
# `*-{iname}` del manifiesto, para el fallback de «hay algún sellado histórico» sin tocar el disco.
|
||||
_REMOTOS_POR_INAME = {}
|
||||
for _d in SELLADOS_REMOTOS:
|
||||
_REMOTOS_POR_INAME.setdefault(_d.split("-", 1)[1] if "-" in _d else _d, []).append(_d)
|
||||
REMOTO_ONLY = set()
|
||||
|
||||
def state_of(iname, h):
|
||||
if h is None:
|
||||
return "unhashable"
|
||||
if os.path.isdir(os.path.join(STORE, f"{h[3:]}-{iname}")):
|
||||
dirname = f"{h[3:]}-{iname}"
|
||||
if os.path.isdir(os.path.join(STORE, dirname)):
|
||||
return "sealed"
|
||||
if dirname in SELLADOS_REMOTOS:
|
||||
REMOTO_ONLY.add(dirname)
|
||||
return "sealed"
|
||||
# ¿hay algún sellado histórico? distingue "cambió" de "nunca construida".
|
||||
if glob.glob(os.path.join(STORE, f"*-{iname}")):
|
||||
if glob.glob(os.path.join(STORE, f"*-{iname}")) or _REMOTOS_POR_INAME.get(iname):
|
||||
return "debt"
|
||||
return "never"
|
||||
|
||||
@@ -282,7 +323,11 @@ def main():
|
||||
schema="hammer-build-state/1",
|
||||
# `recipes` sigue contando SÓLO las que tienen fichero (los consumidores lo leen como el
|
||||
# tamaño del corpus); `nodes` incluye la frontera `wanted`.
|
||||
totals=dict(recipes=n_recetas, nodes=len(recs), **by_state),
|
||||
# `sealed_remoto`: de los `sealed`, cuántos NO están en el disco local y se dan por buenos
|
||||
# por el manifiesto de la granja. Va explícito para que «el store se mudó» no se lea nunca
|
||||
# como «el corpus creció»; si algún día sube sin que nadie haya sembrado, es la señal.
|
||||
totals=dict(recipes=n_recetas, nodes=len(recs), **by_state,
|
||||
sealed_remoto=len(REMOTO_ONLY)),
|
||||
by_class={k: by_cls[k] for k in sorted(by_cls)},
|
||||
debt_by_class={k: debt_by_cls[k] for k in sorted(debt_by_cls)},
|
||||
by_queue={q: dict(c) for q, c in sorted(by_queue.items())},
|
||||
|
||||
@@ -77,14 +77,29 @@ else
|
||||
# bucle por cerrado, y la churn siguió porque **hay DOS rutas que bajan el store** y el latido
|
||||
# usa ésta. Arreglar una de dos copias y declarar victoria es peor que no arreglar: el síntoma
|
||||
# se atenúa lo justo para dejar de mirar. Si aparece una tercera copia, va con el mismo exclude.
|
||||
LEDGER="$ROOT/work/store-gc-superados.txt"
|
||||
EXCL=""
|
||||
[ -s "$LEDGER" ] && EXCL="--exclude-from=$LEDGER"
|
||||
if rsync -az --exclude /.dmerge $EXCL -e "$SSH" "root@$ip:$REMOTE/store/" "$ROOT/store/" 2>&1 | tail -2; then
|
||||
echo " cosecha ✓"
|
||||
#
|
||||
# ── 2026-08-09: EL STORE YA NO BAJA. BAJA EL MANIFIESTO ───────────────────────────────────
|
||||
# El store se mudó al VOLUMEN de la granja y el laptop se quedó con lo que la granja no puede
|
||||
# rehacer. Seguir bajando el store acá desharía esa mudanza cada 30 minutos — la misma forma
|
||||
# exacta del bucle de churn que este bloque documenta arriba, y la razón por la que el exclude
|
||||
# de abajo dejó de alcanzar: ya no se trata de filtrar QUÉ baja, sino de que no baja.
|
||||
#
|
||||
# Lo que el hub sí necesita es SABER qué hay sellado, para que el grafo de estado no reporte
|
||||
# `never` sobre artefactos que existen. Eso son kilobytes: la lista de nombres. Se acumula en
|
||||
# el manifiesto (nunca se pisa) porque el volumen es una sola foto y el hub recuerda todas.
|
||||
if $SSH "root@$ip" "ls $REMOTE/store 2>/dev/null | grep -E '^[0-9a-f]{64}-'" \
|
||||
> "$ROOT/work/.sellados-worker" 2>/dev/null && [ -s "$ROOT/work/.sellados-worker" ]; then
|
||||
N_W=$(wc -l < "$ROOT/work/.sellados-worker")
|
||||
cat "$ROOT/work/farm-sellados.txt" "$ROOT/work/.sellados-worker" 2>/dev/null \
|
||||
| grep -E '^[0-9a-f]{64}-' | sort -u > "$ROOT/work/farm-sellados.txt.new"
|
||||
mv "$ROOT/work/farm-sellados.txt.new" "$ROOT/work/farm-sellados.txt"
|
||||
rm -f "$ROOT/work/.sellados-worker"
|
||||
echo " manifiesto ✓ (worker: $N_W · total: $(wc -l < "$ROOT/work/farm-sellados.txt"))"
|
||||
else
|
||||
echo " ⚠ cosecha falló — sigo"
|
||||
echo " ⚠ no pude leer el manifiesto del worker — sigo"
|
||||
fi
|
||||
# ⚠ Lo que NO cubre esto: los artefactos nuevos quedan SÓLO en el volumen hasta que alguien
|
||||
# haga una pasada de respaldo. El volumen tiene borrado protegido, pero es UNA copia.
|
||||
done < "$FLEET"
|
||||
fi
|
||||
|
||||
|
||||
@@ -24,9 +24,35 @@ SSH="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new"
|
||||
# viaja hub→worker porque son gigabytes, pero la lista de nombres son kilobytes y evita que el worker
|
||||
# reintente y falle cada ciclo lo que el hub ya tiene. Se regenera acá, en cada sync, para que no se
|
||||
# quede vieja sola.
|
||||
#
|
||||
# ── ⚠ EL MANIFIESTO NO ES `ls ./store` ─────────────────────────────────────────────────────────
|
||||
# Lo fue hasta el 2026-08-09, y desde que el store se mudó al VOLUMEN eso pasó a ser una mentira
|
||||
# cara: el disco local se quedó con 220 artefactos (los de fuente privada + lo aún no respaldado),
|
||||
# así que `ls ./store` le habría dicho al worker «el hub tiene 220» y el worker habría reconstruido
|
||||
# ~950 artefactos que ya existen. Es exactamente el bucle que este fichero documenta más abajo, pero
|
||||
# al revés y multiplicado: no devolver basura, sino rehacer lo hecho.
|
||||
#
|
||||
# La pregunta que el worker hace es «¿esto ya está sellado?», no «¿esto está en el disco del hub?».
|
||||
# Se responde con la UNIÓN de lo que hay en el disco y lo que registran los manifiestos — el mismo
|
||||
# criterio que usa `build-state.py`, y por la misma razón: la presencia de los bytes y el hecho de
|
||||
# estar construido dejaron de ser lo mismo el día que el store dejó de vivir acá.
|
||||
mkdir -p work
|
||||
ls ./store 2>/dev/null | grep -E '^[0-9a-f]{64}-' > work/farm-sellados.txt || true
|
||||
echo "==> 0. manifiesto de sellados del hub: $(wc -l < work/farm-sellados.txt) artefactos"
|
||||
{
|
||||
ls ./store 2>/dev/null | grep -E '^[0-9a-f]{64}-' || true
|
||||
# `respaldo-sellados.txt` (lo que hay en el Storage Box) y el propio manifiesto del ciclo anterior,
|
||||
# que arrastra lo que el worker selló y todavía no llegó a ningún otro registro.
|
||||
cat work/respaldo-sellados.txt work/farm-sellados.txt 2>/dev/null || true
|
||||
} | grep -E '^[0-9a-f]{64}-' | sort -u > work/farm-sellados.txt.new
|
||||
# Guardián: el manifiesto sólo puede CRECER salvo poda explícita. Si encoge, algo se perdió y es más
|
||||
# barato abortar el sync que mandar al worker a rehacer un corpus.
|
||||
VIEJO=$(wc -l < work/farm-sellados.txt 2>/dev/null || echo 0)
|
||||
NUEVO=$(wc -l < work/farm-sellados.txt.new)
|
||||
if [ "$NUEVO" -lt "$VIEJO" ]; then
|
||||
echo "⚠ el manifiesto ENCOGIÓ ($VIEJO → $NUEVO) — no sincronizo; revisá antes de que el worker rehaga trabajo" >&2
|
||||
rm -f work/farm-sellados.txt.new; exit 1
|
||||
fi
|
||||
mv work/farm-sellados.txt.new work/farm-sellados.txt
|
||||
echo "==> 0. manifiesto de sellados: $NUEVO artefactos (disco local: $(ls ./store 2>/dev/null | grep -cE '^[0-9a-f]{64}-'))"
|
||||
|
||||
echo "==> 1. subiendo código + cola al worker ($VPS:$REMOTE)"
|
||||
rsync -az --delete -e "$SSH" \
|
||||
|
||||
@@ -47,7 +47,14 @@
|
||||
# deliberada y aparte. El repo sí va con `--delete`, que para eso está git.
|
||||
#
|
||||
# Uso: scripts/respaldo-storagebox.sh [--seco]
|
||||
# scripts/respaldo-storagebox.sh --listar # refresca work/respaldo-sellados.txt y sale
|
||||
# Relanzalo cuantas veces quieras: continúa donde quedó.
|
||||
#
|
||||
# ── `--listar`: el respaldo también es un REGISTRO, no sólo una copia ──────────────────────────
|
||||
# Desde que el store se mudó al volumen (2026-08-09), `build-state.py` y `farm-sync.sh` responden
|
||||
# «¿esto está sellado?» con la unión del disco local y los manifiestos, y el del respaldo es el más
|
||||
# completo de los dos. Sin refrescarlo, recetas que existen y están respaldadas se reportan como
|
||||
# deuda y el worker las rehace. Es barato (una lista de nombres) y no toca un solo byte de datos.
|
||||
set -eu
|
||||
|
||||
SB_USER="${SB_USER:-u647150}"
|
||||
@@ -59,6 +66,22 @@ RAIZ="${RAIZ:-/home/sergio/hammer}"
|
||||
SECO=""
|
||||
[ "${1:-}" = "--seco" ] && SECO="--dry-run"
|
||||
|
||||
if [ "${1:-}" = "--listar" ]; then
|
||||
cd "$RAIZ"; mkdir -p work
|
||||
ssh -4 -p "$SB_PORT" -i "$KEY" -o StrictHostKeyChecking=accept-new \
|
||||
"$SB_USER@$SB_HOST" 'ls hammer/store' 2>/dev/null \
|
||||
| grep -E '^[0-9a-f]{64}-' | sort -u > work/respaldo-sellados.txt.new
|
||||
N=$(wc -l < work/respaldo-sellados.txt.new)
|
||||
# Un listado vacío es un fallo de red disfrazado de respaldo borrado. No pisa nada.
|
||||
if [ "$N" -eq 0 ]; then
|
||||
echo "⚠ el box devolvió 0 artefactos — no piso el manifiesto (¿enlace caído?)" >&2
|
||||
rm -f work/respaldo-sellados.txt.new; exit 1
|
||||
fi
|
||||
mv work/respaldo-sellados.txt.new work/respaldo-sellados.txt
|
||||
echo "==> manifiesto del respaldo: $N artefactos → work/respaldo-sellados.txt"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# `-4` a propósito: el box tiene AAAA y el laptop tiene IPv6 sólo en wlan0, así que sin esto la ruta
|
||||
# por cable nunca se usaría aunque el cable estuviera enchufado.
|
||||
SSH_CMD="ssh -4 -p $SB_PORT -i $KEY -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30"
|
||||
|
||||
Reference in New Issue
Block a user